전개 연산자와 배열의 내장 함수를 이용해 간단하게 배열 혹은 객체를 복사하고 새로운 값을 덮어쓸 수 있습니다. 그러나 객체 구조가 엄청나게 깊어지면 불변성을 유지하며 이를 업데이트하기 어렵습니다. 이번 장에서는 immer라는 라이브러리를 사용하여 구조가 복잡한 객체도 쉽고 짧은 코드로 불변성을 유지하면서 업데이트하는 것을 배워보도록 하겠습니다.
immer 라이브러리를 사용하기 위해서는 yarn을 통해 설치해야 합니다.이번 예제로 쓸 리액트 프로젝트와 함께 immer을 설치해 주세요.
$ yarn create react-app immer-tutorial
$ cd immer-tutorial
$ yarn add 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
});
// 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
};
setData({
...data,
array: data.array.concat(info)
});
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 함수가 불변성 유지를 대신하면서 새로운 상태를 생성합니다. 이 라이브러리의 핵심은 ‘불변성에 신경 쓰지 않는 것처럼 코드를 작성하되 불변성 관리는 제대로 해주는 것’입니다.
이제 app.js에 immer를 적용시켜 봅시다.
mport 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
});
// 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
};
setData(
produce(data, draft => {
draft.array.push(info);
})
);
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의 경우 배열 내장 함수 filter를 사용하는 것이 코드가 더 깔끔합니다. 불변성을 유지하는 코드가 복잡할 때만 immer를 사용해도 괜찮습니다.
immer에서 제공하는 produce 함수를 호출할 때, 첫 번째 파라미터가 함수 형태라면 업데이트 함수를 반환합니다.
const update = (draft => {
draft.value = 2;
});
const originalState = {
value: 1,
foo: ‘bar‘,
};
const nextState = update(originalState);
console.log(nextState); // { value: 2, foo: ‘bar‘ }
이런 immer의 속성과 11장에서 배운 useState의 함수형 업데이트를 함께 활용하여 더욱 깔끔한 코드를 만들 수 있습니다. 아래처럼 코드를 수정해 보세요.
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
});
// 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 (...);
};
export default App;
produce 함수의 파라미터를 함수 형태로 사용하여 코드를 더욱 깔끔하게 작성할 수 있습니다.
컴포넌트의 상태 업데이트가 까다로울 때 immer를 통해 쉽게 작성할 수 있습니다. 추후 상태 관리 라이브러리인 리덕스를 배워 사용할 때도 immer를 사용할 수 있습니다. 하지만 굳이 immer를 사용하지 않아도 되는 상황이거나 오히려 immer이 불편하다면 사용하지 않아도 좋습니다.
Quiz
1. ( A ) 라이브러리를 사용하면, 구조가 복잡한 객체도 매우 쉽게 짧은 코드를 사용하여 불변성을 유지하면서 업데이트할 수 있다.
2. produce 함수는 두 가지 파라미터를 갖는데, 첫 번째 파라미터는 ( B )이고, 두 번째 피라미터는 ( C )이다.
3. immer를 사용하여 컴포넌트 상태를 작성할 때는 객체 안에 있는 값을 직접 수정하거나, 배열에 직접적인 변화를 일으키는 ( D ), ( E ) 등의 함수를 사용해도 무방하다.
4. immer에서 제공하는 produce 함수를 호출할 때, 첫 번째 파라미터가 함수 형태라면 ( F )를 반환한다.
5. 4번의 속성에 따라서 ( G ) 를 함께 활용하면 코드를 더욱 깔끔하게 만들 수 있다.
6. 'immer 라이브러리를 사용하면 항상 코드가 간결해진다.'는 ( H )이다.
7. immer 라이브러리의 핵심은 '( I )에 신경 쓰지 않는 것처럼 코드를 작성하되 ( I ) 관리는 제대로 해주는 것'이다.
8. 아래 코드를 immer를 적용하여 깔끔하게 수정하세요.
// 8번
const onChange = useCallback(
e => {
const { age, value } = e.target;
setForm({
…form,
[age]: [value]
});
},
[form]
);
9. 아래 코드를 useState의 함수형 업데이트를 활용하여 깔끔하게 수정하세요.
// 9번
const onRemove = useCallback(
number => {
setData(
produce(data, draft => {
draft.array.splice(draft.array.findIndex(info => info.number === number), 1);
})
);
},
[data]
);
Corner React1
Editor: 이조
[리액트 스타터1] 16장. 리덕스 라이브러리 이해하기 (0) | 2023.01.12 |
---|---|
[리액트 스타터1] 13장. 리액트 라우터로 SPA 개발하기 (0) | 2023.01.05 |
[리액트 스타터1] 11장. 컴포넌트 성능 최적화 (0) | 2022.12.29 |
[리액트 스타터1] 10장 : 일정 관리 웹 애플리케이션 만들기 (0) | 2022.12.22 |
[리액트 스타터1] 9장. 컴포넌트 스타일링 (0) | 2022.12.01 |