[React]useImmer 라이브러리 사용법
Immer 라이브러리는 주로 객체를 업데이트 하는 용도로 사용된다. 리액트에서는 state 객체를 업데이트 할 때 불변성을 관리하는 용도로 사용될 수 있는데, 기본적으로 state 객체를 업데이트 할 때는 다음과 같은 방법을 사용한다.
import { useState } from 'react';
export default function StateForm() {
const [form, setForm] = useState({
name: '홍길동',
address: {
do: '경기도',
city: '성남',
},
});
const handleForm = (e) => {
const { name, value } = e.target;
setForm({
// 기존의 객체를 그대로 유지하기 위해 스프레드 구문 사용
...form,
[name]: value,
});
};
return (
<form>
<div>
<label htmlFor='name'>이름</label>
<input id='name' name='name' type='text' onChange={handleForm} value={form.name} />
</div>
<div>
<label htmlFor='do'>도</label>
<input id='do' name='do' type='text' onChange={handleForm} value={form.address.do} />
</div>
<div>
<label htmlFor='city'>시</label>
<input
id='city'
name='city'
type='text'
onChange={handleForm}
value={form.address.city}
/>
</div>
</form>
);
}
하지만 이렇게 코드를 작성할 경우 스프레드 구문은 객체를 얕은 복사하기 때문에 do와 city를 업데이트 하는 경우 다음과 같이 address의 객체도 스프레드 구문을 사용해서 복사를 해줘야 한다.
const handleFormNest = (e) => {
const { name, value } = e.target;
setForm({
...form,
address: {
...form.address,
[name]: value,
},
});
};
<div>
<label htmlFor='do'>도</label>
<input id='do' name='do' type='text' onChange={handleFormNest} value={form.address.do} />
</div>
<div>
<label htmlFor='city'>시</label>
<input
id='city'
name='city'
type='text'
onChange={handleFormNest}
value={form.address.city}
/>
</div>
하지만 만약 객체의 깊이가 깊어지는 경우 불변성 관리를 하기 매우 힘들어진다. 어떤 이유로 State 객체의 깊이를 제외할 수 없는 경우에는 Immer 라이브러리를 활용하면 보다 직관적으로 중첩된 객체를 업데이트할 수 있다.
Immer를 사용하기 위해서는 다음 명령어를 통해 라이브러리를 프로젝트에 미리 포함시켜야 한다.
npm install use-immer
use-immer는 Immer를 React에서 사용하기 위한 라이브러리이며, use-immer를 설치하면 Immer도 함께 설치된다.
라이브러리 설치가 완료되면 Immer를 사용하여 다시 작성해 보자.
import { useImmer } from 'use-immer';
export default function FormFile() {
const [form, setForm] = useImmer({
name: '홍길동',
address: {
do: '경기도',
city: '성남',
},
});
const handleForm = (e) => {
const { name, value } = e.target;
setForm((form) => {
form[name] = value;
});
};
const handleFormNest = (e) => {
const { name, value } = e.target;
setForm((form) => {
form.address[name] = value;
});
};
...
}
useImmer 함수의 사용법은 useState 함수와 유사하므로 특별히 언급할 사항을 없다. 인수로 State의 초깃값을 전달하고, 반환값으로 [State값, State의 세터]를 받는다.
다른 점은 세터의 사용법이다. useImmer 함수로 생성된 세터에서는 세터의 용도가 다음과 같은 성격을 가지고 있다.
- State를 인수로 받는다.
- 함수 내부에서 State를 업데이트할 수 있다.
세터 아래에서 변경된 내용은 내부적으로 복제된 State에 반영되는데, 지금까지 스프레드 구문으로 표현하던 부분을 대신해 주는 것이다. address 내부를 변경하는 코드에서도 거의 비슷하게 작성할 수 있어 코드가 훨씬 간단해졌다.
핸들러 공통화하기
위의 예시에서는 계층에 따라서 별도의 핸들러를 만들어 줬지만 다음과 같이 통일할 수 있다.
import { useImmer } from 'use-immer';
export default function FormFile() {
const [form, setForm] = useImmer({
name: '홍길동',
address: {
do: '경기도',
city: '성남',
},
});
const handleNest = (e) => {
// 요소명을 '.'으로 분해(요소 이름이 'xxxx.xxxx'라는 가정 하에)
const ns = e.target.name.split('.');
setForm((form) => {
// 계층에 따라 대위임처를 변경한다.
if (ns.length === 1) {
form[ns[0]] = e.target.value;
} else {
form[ns[0]][ns[1]] = e.target.value;
}
});
};
return (
<form>
<div>
<label htmlFor='name'>이름</label>
<input id='name' name='name' type='text' onChange={handleNest} value={form.name} />
</div>
<div>
<label htmlFor='do'>도</label>
<input
id='do'
name='address.do'
type='text'
onChange={handleNest}
value={form.address.do}
/>
</div>
<div>
<label htmlFor='city'>시</label>
<input
id='city'
name='address.city'
type='text'
onChange={handleNest}
value={form.address.city}
/>
</div>
</form>
);
}
name 속성에 계층에 따른 이름을 붙여줌으로써 하나의 핸들러로 2계층까지 업데이트하도록 구현하였다. 만약에 3계층이상 업데이트를 해야하는 경우 분기만 추가해주면 된다.