안녕하세요, 코린이의 코딩 학습기 채니 입니다.
[리액트를 다루는 기술]의 책을 참고하여 포스팅한 개인 공부 내용입니다.
immer
- 구조가 복잡한 객체도 매우 쉽고 짧은 코드를 사용해 불변성을 유지하며 업데이트해주는 라이브러리
리액트는 상태를 업데이트할 때 반드시 불변성을 유지해야합니다.
하지만, 아래와 같이 객체의 구조가 깊어진다면 업데이트하는 것은 굉장히 번거로울 것입니다.
immer 사용하지 않을 때 예시
const object = {
somewhere: {
deep: {
inside: 3,
array: [1, 2, 3, 4]
},
bar: 2
},
foo: 1
};
// somewhere.deep.inside 값을 4로 바꾸기
let nextObject = {
...object,
somewhere: {
...object.somewhere,
deep: {
...object.somewhere.deep,
inside: 4
}
}
};
// somewhere.deep.array에 5추가
let nextObject = {
...object,
somewhere: {
...object.somewhere,
deep: {
...object.somewhere.deep,
array: object.somewhere.deep.array.concat(5)
}
}
};
코드가 굉장히 복잡해지는 것을 확인할 수 있습니다. 가독성 또한 좋지 않습니다.
이를 immer 라이브러리를 통해 해결할 수 있습니다.
immer 설치 및 사용법
프로젝트 준비
$ yarn create react-app immer-tutorial
$ cd immer-tutorial
$ yarn add immer
immer-tutorial 프로젝트 생성 후 immer 라이브러리를 설치하였습니다.
immer를 사용하지 않고 불변성 유지
차이를 알아보기 위해, 먼저 immer를 사용하지 않고 불변성 유지를 해보겠습니다.
App.js (immer 사용 X)
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;
폼에서 아이디/이름 입력 후 등록 버튼을 누르면 리스트에 추가되고, 리스트 항목 클릭 시 해당 항목이 삭제됩니다.
위처럼 전개 연산자 및 함수를 사용해 불변성을 유지하는 것은 어렵진 않지만, 만일 상태가 복잡해진다면 귀찮을 수도 있겠죠.
immer 사용법
예시
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);
})
App 컴포넌트에 immer 적용하기
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;
// immer 사용
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 새 항목 등록
// immer 사용
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]);
...
...
immer를 사용하여 객체의 값을 수정하거나 배열에 직접적으로 접근하여 추가하는 push, splice 함수를 사용한 것을 확인할 수 있습니다.
draft가 변경하려고 하는 객체 혹은 배열의 복사본을 가지고 변경해주는 함수다! 라고 쉽게 생각하면 이해가 쉽습니다.
useState의 함수형 업데이트와 immer 함께 쓰기
useState의 함수형 업데이트
const [number, setNumber] = useState(0);
//prevNumber는 현재 number를 가리킴
const onIncrease = useCallback(
() => setNumber(prevNumber => prevNumber + 1), []
);
immer에서 제공하는 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' }
이렇게 immer의 속성과 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;
// 함수형 업데이트 + immer 사용
setForm(
produce(draft => {
draft[name] = value;
})
)
}, []); // [form] => []로 변경
// form 등록
const onSubmit = useCallback(e => {
e.preventDefault(); // 폼 제출 방지
const info = {
id: nextId.current,
name: form.name,
username: form.username
};
// array 새 항목 등록
// 함수형 업데이트 + immer 사용
setData(
produce(draft => {
draft.array.push(info);
})
)
// form 초기화
setForm({
name: '',
username: ''
});
nextId.current += 1;
}, [form.name, form.username]); // [data, form.name, form.username] -> [form.name, form.username]로 변경
// 항목 삭제
const onRemove = useCallback(id => {
// 함수형 업데이트 + immer 사용
setData(
produce(draft => {
draft.array.splice(draft.array.findIndex(info => info.id === id), 1)
})
);
}, []); // [data] -> []로 변경
produce 함수의 파라미터를 함수 형태로 사용하니 코드가 깔끔해진 것을 확인할 수 있습니다.