전개 연산자와 배열의 내장 함수를 사용하면 간단하게 배열 혹은 객체를 복사하고 새로운 값을 덮어 쓸 수 있다. 하지만 객체의 구조가 복잡하고 깊어질수록 불변성을 유지하며 컴포넌트를 업데이트 하는 것이 매우 힘들어진다. 이런 경우 사용할 수 있는 라이브러리로 immer
라는 것이 있다.
npm add immer
로 라이브러리를 설치해 주고, 사용 방법을 알아보자.
import produce from 'immer';
const nextState = produce(originalState, draft => {
// 바꾸고 싶은 값 바꾸기
draft.somewhere.deep.inside = 5;
})
immer
의 produce
함수의 두 파라미터 중 첫째는 수정하고 싶은 상태이고, 둘째는 상태를 어떻게 업데이트 할지 정의하는 함수이다. 위 코드처럼 전달 함수 내부에서 원하는 값을 변경하면 produce
함수가 불변성 유지를 대신해 주면서 새로운 상태를 생성해 준다.
먼저 immer 없이 불변성을 유지하면서 값을 업데이트하는 컴포넌트를 작성해보자.
App.js
import React, { useRef, useCallback, useState } from 'react';
const App = () => {
const nextId = useRef(1);
const [form, setForm] = useState({ name: '', username: ''});
const [data, setData] = useState({
array: [],
uselessValue: null
});
const onChange = useCallback(
e=>{
const { name, value } = e.target;
setForm({
...form,
[name]: [value]
});
},
[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;
이제 여기에 immer
를 적용해서 더 깔끔한 코드로 상태를 업데이트하자.
App.js
import React, { 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
});
const onChange = useCallback(
e=>{
const { name, value } = e.target;
setForm(
produce(form, 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(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 (...);
};
export default App;
immer
를 사용하여 컴포넌트 상태를 작성할 때는 객체 안에 있는 값을 직접 수정하거나, 배열에 직접적인 변화를 일으키는 push
, splice
등의 함수를 사용해도 무방하다. 하지만 onRemove
와 같이 immer
를 사용한다고 해서 코드가 간결해지지 않는 경우도 있다. 여기서 좀 더 간결하게 하기 위해 useState
의 함수형 업데이트도 진행해보았다.
App.js
(...)
const onChange = useCallback(
e=>{
const { name, value } = e.target;
setForm(
produce(draft => {
draft[name] = value;
})
);
},[]);
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);
})
);
},
[]
);
(...)