안녕하세요! delay100입니다. 이번 포스팅에서는 immer을 사용해 쉽게 불변성을 유지하는 방법에 대해 공부해봅시다.
대부분의 설명은 주석으로 달아놓았으니 코드에 대한 설명은 주석을 확인해주세요!
책 리액트를 다루는 기술, 개정판의 12장 내용을 다루고 있습니다.
이번 포스팅의 Github 링크
https://github.com/delay-100/study-react/tree/main/ch12/immer-tutorial
1. immer?
immer란?
- 쉽고 짧은 코드로 구조가 복잡한 객체에 대해 불변성을 유지하며 업데이트를 도와주는 라이브러리
- 불변성 유지 코드가 너무 긴 경우에 사용함
- 컴포넌트의 상태 업데이트가 까다로울 때 사용
- immer를 사용하여 컴포넌트 상태를 작성 시, 객체 안에 있는 값을 직접 수정하거나 배열에 직접적인 변화를 일으키는 push, splice 등의 함수를 사용해도 무방함
- 불변성에 신경 쓰지 않는 것처럼 코드를 작성하되 불변성 관리는 제대로 해주는 것이 핵심
- 단순히 깊은 곳에 위치하는 값을 바꾸는 것 외에 배열을 처리할 때도 매우 쉽고 편함
*불변성을 지킨다: 기존의 값을 직접 수정하지 않으면서 새로운 값을 만들어 낸다
1-1. immer 설치하기
immer 설치 명령어
yarn add immer
2. immer 사용하기
2-1. 기존의 방법
immer을 사용하기 전에, 기본적으로 전개 연산자와 배열 내장 함수를 사용하는 방법의 예시를 가지고 왔습니다.
- 전개 연산자& 배열 내장 함수 사용 실행 - src/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;
2-2. immer 적용
immer을 바로 적용하기 전에, 기본적인 사용법에 대해 알아봅시다.
immer 사용법
- immer을 사용한 불변성 유지 예시1 + produce 함수 설명
import produce from 'immer';
const nextState = produce(originalState, draft => { // produce 함수는 2가지의 파라미터를 받음(첫 번째 파라미터: 수정하고 싶은 상태, 두 번째 파라미터: 상태를 어떻게 업데이트할 지 정의하는 함수)
// 바꾸고 싶은 값 바꾸기
// 두 번째 파라미터로 전달되는 함수 내부에서 원하는 값을 변경하면 produce 함수가 불변성 유지를 해주면서 새로운 상태를 생성해줍니다.
draft.somewhere.deep.inside = 5;
})
- immer을 사용한 불변성 유지 예시2
import produce from "immer";
const originalState = [
{
id: 1,
todo: "전개 연산자와 배열 내장 함수로 불변성 유지하기",
checked: true,
},
{
id: 2,
todo: "immer로 불변성 유지하기",
checked: false,
},
];
const nextState = produce(originalState, (draft) => {
// 두 번째 파라미터로 전달되는 함수 내부에서 원하는 값을 변경하면 produce 함수가 불변성 유지를 해주면서 새로운 상태를 생성해줍니다.
// draft.somewhere.deep.inside = 5;
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
);
});
2-1 전개 연산자& 배열 내장 함수 사용 실행 코드에 immer을 적용해보겠습니다.
- 2-1 전개 연산자& 배열 내장 함수 사용 실행 코드에 immer 적용 - src/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;
})
);
// // 기존 코드
// setForm({
// ...form,
// [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); // immer를 사용하여 컴포넌트 상태를 작성 시, 객체 안에 있는 값을 직접 수정하거나 배열에 직접적인 변화를 일으키는 push, splice 등의 함수를 사용해도 무방함
})
);
// 기존 코드
// setData({
// ...data,
// array: data.array.concat(info),
// });
// form 초기화
setForm({
name: "",
username: "",
});
nextId.current += 1;
},
[data, form.name, form.username]
);
// 항목을 삭제하는 함수
const onRemove = useCallback(
(id) => {
// immer 적용
setData(
produce(data, (draft) => {
draft.array.splice(
draft.array.findIndex((info) => info.id === id),
1
);
})
);
// 기존 코드
// 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;
실행 결과는 2-1 전개 연산자& 배열 내장 함수 사용 실행 결과와 동일합니다.
2-3. useState 함수형 업데이트와 같이 쓰기
이전 포스팅에서 useState의 함수형 업데이트(2-2 useState 성능개선)에 대해 공부했습니다.
- useState 함수형 업데이트 예시
const [number, setNumber] = useState(0);
// prevNumbers는 현재 number 값을 가리킵니다.
const onIncrease = useCallback(
() => setNumber(prevNumber => prevNumber + 1),
[],
);
immer에서 제공하는 produce함수를 호출할 때, 첫 번째 파라미터가 함수 형태라면 업데이트 함수를 반환합니다.
const update = produce(draft => { // 첫 번째 파라미터가 함수형태라면, 업데이트 함수 반환
// produce 함수는 2가지의 파라미터를 받음(첫 번째 파라미터: 수정하고 싶은 상태, 두 번째 파라미터: 상태를 어떻게 업데이트할 지 정의하는 함수)
draft.value = 2;
});
const originalState = {
value: 1,
foo: 'bar',
};
const nextState = update(originalState);
console.log(nextState); // { value: 2, foo: 'bar'}
이러한 immer의 속성과 useState의 함수형 업데이트를 함께 활용해 코드를 더욱 깔끔하게 만들어봅시다.
2-2 의 2-1전개 연산자& 배열 내장 함수 사용 실행코드에 immer 적용 코드에 useState의 함수형 업데이트를 추가해보겠습니다.
- 2-2의 App.js코드에 useState의 함수형 업데이트를 추가 - src/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) => {
// useState의 함수형 업데이트와 immer 함께 쓰기
produce((draft) => {
draft[name] = value;
})
);
// // 기존 코드
// setForm({
// ...form,
// [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) => {
// useState의 함수형 업데이트와 immer 함께 쓰기
produce((draft) => {
draft.array.push(info); // immer를 사용하여 컴포넌트 상태를 작성 시, 객체 안에 있는 값을 직접 수정하거나 배열에 직접적인 변화를 일으키는 push, splice 등의 함수를 사용해도 무방함
})
);
// 기존 코드
// setData({
// ...data,
// array: data.array.concat(info),
// });
// form 초기화
setForm({
name: "",
username: "",
});
nextId.current += 1;
},
[form.name, form.username]
// [data, form.name, form.username]
);
// 항목을 삭제하는 함수
const onRemove = useCallback(
(id) => {
// immer 적용
setData(
// produce(data, (draft) => {
// useState의 함수형 업데이트와 immer 함께 쓰기
produce((draft) => {
draft.array.splice(
draft.array.findIndex((info) => info.id === id),
1
);
})
);
// 기존 코드
// 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;
실행 결과는 2-1 전개 연산자& 배열 내장 함수 사용 실행 결과와 동일합니다.
읽어주셔서 감사합니다. 잘못된 정보는 댓글로 알려주세요!
'Study > React' 카테고리의 다른 글
14. 뉴스 뷰어 만들기(with. newsapi) (0) | 2022.07.27 |
---|---|
13. Router(라우터) & SPA(Single Page Application) (0) | 2022.07.24 |
11. 컴포넌트 성능 최적화 (0) | 2022.07.20 |
10. 간단한 투두리스트(TodoList) 만들기 (0) | 2022.07.19 |
9. 컴포넌트 스타일링(CSS) (0) | 2022.07.18 |