1.5 useState의 함수형 업데이트와 immer 함께 쓰기
구조가 복잡한 객체도 매우 쉽고 짧은 코드를 사용해 불변성을 유지하며 업데이트할 수 있게 해주는 라이브러리
터미널에서 다음 명령을 수행한다.
$ yarn create react-app immer-tutorial
$ cd immer-tutorial
$ yarn add immer
immer를 사용하지 않고 불변성을 유지하면서 값을 업데이트하는 컴포넌트를 작성해보자.
// App.js
import { useRef, useCallback, useState } from 'react';
const App = () => {
const nextId = useRef(1);
const [form, setForm] = useState({ name: '', username: '' });
const [data, setData] = useState({
array: [],
uselessValue: null
});
// input 수정을 위한 함수
const onChange = useCallback(e => {
const { name, value } = e.target;
setForm(
{
...form,
[name]: [value]
}
);
}, [form]);
// form 등록을 위한 함수
const onSubmit = useCallback(
e => {
e.preventDefault();
const info = {
id: nextId.current,
name: form.name,
username: form.username
};
// array 에 새 항목 등록
setData({
...data,
array: data.array.concat(info)
});
// form 초기화
setForm({
name: '',
username: ''
});
nextId.current += 1;
},
[data, form.name, form.username]
);
// 항목을 삭제하는 함수
const onRemove = useCallback(
id => {
setData({
...data,
array: data.array.filter(info => info.id !== id)
});
},
[data]
);
return (
<div>
<form onSubmit={onSubmit}>
<input
name="username"
placeholder="아이디"
value={form.username}
onChange={onChange}
/>
<input
name="name"
placeholder="이름"
value={form.name}
onChange={onChange}
/>
<button type="submit">등록</button>
</form>
<div>
<ul>
{data.array.map(info => (
<li key={info.id} onClick={() => onRemove(info.id)}>
{info.username} ({info.name})
</li>
))}
</ul>
</div>
</div>
);
};
export default App;
이 컴포넌트의 기능
폼에서 아이디/이름 입력 시 ▶ 하단 리스트에 추가
리스트 항목 클릭 시 ▶ 삭제
// 예시 코드: 구조가 깊은 객체에서 값을 변경할 때
import produce from 'immer';
const nextState = produce(originalState, draft => {
// 바꾸고 싶은 값 바꾸기
draft.somewhere.deep.inside = 5;
})
produce(상태, 함수)
'불변성에 신경 쓰지 않는 것처럼 코드를 작성하되, 불변성 관리는 제대로 해주는 것'
produce 함수가 불변성 유지 작업을 대신해주면서 새로운 상태를 생성해주므로,
프로그래머는 쉽고 짧은 코드로도 불변성을 유지하며 원하는 값만 업데이트할 수 있다.
깊은 곳에 위치하는 값을 바꾸는 경우 외에, 배열을 처리할 때에도 매우 쉽고 편하다.
// 예시 코드: 배열, 객체를 모두 관리하는 복잡한 데이터
import produce from 'immer';
const originalState = [
{
id: 1,
todo: '전개 연산자와 배열 내장 함수로 불변성 유지하기',
checked: true,
},
{
id: 2,
todo: 'immer로 불변성 유지하기',
checked: false,
}
];
const nextState = produce(originalState, draft => {
// id가 2인 항목의 checked 값을 true로 설정
const todo = draft.find(t => t.id === 2); // id로 항목 찾기
todo.checked = true; // 혹은 draft[1].checked = true;
// 배열에 새로운 데이터 추가
draft.push({
id: 3,
todo: '일정 관리 앱에 immer 적용하기',
checked: false,
});
// id = 1인 항목을 제거하기
draft.splice(draft.findIndex(t => t.id === 1), 1);
});
1.2에서 만든 App 컴포넌트에 immer를 적용해 더욱 깔끔한 코드로 만들어보자.
import { useRef, useCallback, useState } from 'react';
import produce from 'immer';
const App = () => {
const nextId = useRef(1);
const [form, setForm] = useState({ name: '', username: '' });
const [data, setData] = useState({
array: [],
uselessValue: null
});
// input 수정을 위한 함수
const onChange = useCallback(e => {
const { name, value } = e.target;
setForm(
produce(form, draft => {
draft[name] = value;
})
);
}, [form]);
// form 등록을 위한 함수
const onSubmit = useCallback(
e => {
e.preventDefault();
const info = {
id: nextId.current,
name: form.name,
username: form.username
};
// array 에 새 항목 등록
setData(
produce(data, draft => {
draft.array.push(info);
})
);
// form 초기화
setForm({
name: '',
username: ''
});
nextId.current += 1;
},
[data, form.name, form.username]
);
// 항목을 삭제하는 함수
const onRemove = useCallback(
id => {
setData(
produce(data, draft => {
draft.array.splice(draft.array.findIndex(info => info.id === id), 1);
})
);
},
[data]
);
return (
<div>
<form onSubmit={onSubmit}>
<input
name="username"
placeholder="아이디"
value={form.username}
onChange={onChange}
/>
<input
name="name"
placeholder="이름"
value={form.name}
onChange={onChange}
/>
<button type="submit">등록</button>
</form>
<div>
<ul>
{data.array.map(info => (
<li key={info.id} onClick={() => onRemove(info.id)}>
{info.username} ({info.name})
</li>
))}
</ul>
</div>
</div>
);
};
export default App;
immer를 사용하여 컴포넌트 상태를 작성할 때
immer를 사용한다고 해서 무조건 코드가 간결해지지는 않는다. 복잡한 코드를 정리할 때에만 사용해도 충분하다.
예) 위 예시의 onRemove의 경우 배열 내장 함수 filter를 사용하는 코드가 더 깔끔하므로, immer를 적용할 필요가 없다.
11장에서 공부한 useState의 함수형 업데이트
const [number, setNumber] = useState(0);
// prevNumbers는 현재 number 값을 가리킨다.
const onIncrease = useCallback(
() => setNumber(prevNumber => prevNumber +1),
[],
);
immer에서 제공하는 produce 함수를 호출할 때, 첫 번째 파라미터가 함수 형태라면 produce()는 업데이트 함수를 반환한다.
const update = produce(draft => {
draft.value = 2;
});
const originalState = {
value: 1,
foo: 'bar',
};
const nextState = update(originalState);
console.log(nextState); // { value: 2, foo: 'bar' }
이렇게 반환된 업데이트 함수와 useState의 함수형 업데이트를 함께 활용하면 코드를 더욱 깔끔하게 만들 수 있다.
// App.js
import { useRef, useCallback, useState } from 'react';
import produce from 'immer';
const App = () => {
const nextId = useRef(1);
const [form, setForm] = useState({ name: '', username: '' });
const [data, setData] = useState({
array: [],
uselessValue: null
});
// input 수정을 위한 함수
const onChange = useCallback(e => {
const { name, value } = e.target;
setForm(
produce(draft => {
draft[name] = value;
})
);
}, []);
// form 등록을 위한 함수
const onSubmit = useCallback(
e => {
e.preventDefault();
const info = {
id: nextId.current,
name: form.name,
username: form.username
};
// array 에 새 항목 등록
setData(
produce(draft => {
draft.array.push(info);
})
);
// form 초기화
setForm({
name: '',
username: ''
});
nextId.current += 1;
},
[form.name, form.username]
);
// 항목을 삭제하는 함수
const onRemove = useCallback(
id => {
setData(
produce(draft => {
draft.array.splice(draft.array.findIndex(info => info.id === id), 1);
})
);
},
[]
);
return (
<div>
<form onSubmit={onSubmit}>
<input
name="username"
placeholder="아이디"
value={form.username}
onChange={onChange}
/>
<input
name="name"
placeholder="이름"
value={form.name}
onChange={onChange}
/>
<button type="submit">등록</button>
</form>
<div>
<ul>
{data.array.map(info => (
<li key={info.id} onClick={() => onRemove(info.id)}>
{info.username} ({info.name})
</li>
))}
</ul>
</div>
</div>
);
};
export default App;
1. immer 라이브러리를 사용하면 간단한 코드만으로 (불변성을 유지)하며 원하는 값을 업데이트할 수 있다.
2. 기존의 방법으로는 객체, 배열로 이루어진 복잡한 데이터를 다룰 때 (전개 연산자(...))와 (배열 내장 함수)를 여러 번 사용해야 했다.
3. immer 라이브러리의 (produce) 함수는 첫 번째 파라미터로 (수정하고 싶은 상태), 두 번째 파라미터로 (상태를 어떻게 업데이트할지 정의하는 함수)를 받는다.
4. immer를 사용하여 컴포넌트 상태를 작성할 때는 객체 안에 있는 값을 (직접 수정하거나), 배열에 직접적인 변화를 일으키는 push, splice 등의 (배열 내장 함수)를 사용해도 무방하다.
5. immer를 사용했을 때 항상 코드가 (간결해지는 것은) 아니므로 불변성을 유지하는 코드가 (복잡할) 때에만 사용하면 된다.
6. produce 함수의 첫 번째 파라미터가 함수 형태라면 (업데이트 함수)를 반환한다.
7. 6. 같은 특성과 (useState)의 (함수형 업데이트)를 함께 활용하면 코드를 더욱 깔끔하게 할 수 있다.
1. 다음 컴포넌트에 immer 라이브러리를 적용해 간결하게 만든다.
2. useState의 함수형 업데이트를 함께 사용해 코드를 더욱 간단하게 만든다.
import { useCallback, useState } from 'react';
const App = () => {
const [student, setStudent] = useState({ name: '', ID: '' });
const [data, setData] = useState({
array: [],
uselessValue: null
});
// input 수정을 위한 함수
const onChange = useCallback(e => {
const { name, value } = e.target;
setStudent(
{
...student,
[name]: [value]
}
);
}, [student]);
// form 등록을 위한 함수
const onSubmit = useCallback(
e => {
e.preventDefault();
const info = {
id: student.ID,
name: student.name,
};
// array 에 새 항목 등록
setData({
...data,
array: data.array.concat(info)
});
// form 초기화
setStudent({
name: '',
ID: ''
});
},
[data, student.name, student.ID]
);
// 항목을 삭제하는 함수
const onRemove = useCallback(
id => {
setData({
...data,
array: data.array.filter(info => info.id !== id)
});
},
[data]
);
return (
<div>
<form onSubmit={onSubmit}>
<input
name="ID"
placeholder="학번"
value={student.ID}
onChange={onChange}
/>
<input
name="name"
placeholder="이름"
value={student.name}
onChange={onChange}
/>
<button type="submit">등록</button>
</form>
<div>
<ul>
{data.array.map(info => (
<li key={info.id} onClick={() => onRemove(info.id)}>
{info.id} / {info.name}
</li>
))}
</ul>
</div>
</div>
);
};
export default App;
Corner React Starter #2
Editor 유즈
<리액트를 다루는 기술> 16장: 리덕스 라이브러리 이해하기 (0) | 2022.01.31 |
---|---|
<리액트를 다루는 기술> 13장: 리액트 라우터로 SPA 개발하기 (0) | 2022.01.24 |
<리액트를 다루는 기술> 11장: 컴포넌트 성능 최적화 (0) | 2022.01.17 |
<리액트를 다루는 기술> 10장: 일정 관리 웹 애플리케이션 만들기 (0) | 2022.01.10 |
<리액트를 다루는 기술> 9장: 컴포넌트 스타일링 (0) | 2022.01.03 |