리액트에서 불변성을 유지하는 것은 상태를 효율적으로 업데이트하고 컴포넌트의 성능을 최적화하기 위해 중요하다. 불변성을 유지하면 상태를 직접 수정하지 않고 복제본을 만들어 변경하기 때문에, 이전 버전과 새 버전의 상태를 비교해 변경된 부분만 업데이트할 수 있다.
readonly
타입스크립트에서는 불변성을 유지하는 가장 일반적인 방법으로 'readonly' 키워드를 사용하는 방법이 있다. 예를 들어, 다음과 같이 Person 타입의 객체를 만들어 불변성을 유지할 수 있다.
type Person = {
readonly name: string;
readonly age: number;
}
이렇게 하면 Person 객체의 name과 age 프로퍼티는 읽기 전용(readonly)으로 만들어지기 때문에, 객체의 상태를 변경할 수 없다. 대신, 새로운 객체를 만들어 원하는 값을 변경하고 변경된 새 객체를 반환해야한다.
Object.freeze()
또 다른 방법으로는 Object.freeze() 메서드를 사용하는 것이다. 이 메서드는 객체를 불변하게 만들어주며, 다음과 같이 person 객체를 불변하게 만들 수 있다.
const person = Object.freeze({
name: 'John',
age: 30,
});
이렇게 하면 person 객체의 프로퍼티를 변경할 수 없다.
immer
immer는 불변성을 유지하는 작업을 쉽게 하기 위한 라이브러리다. 리액트와 함께 사용할 수 있으며, 상태 업데이트를 보다 간결하고 가독성 좋게 작성할 수 있다.
immer는 상태를 변경하는 작업을 함수형으로 작성하며, 내부적으로 불변성을 유지하면서 새로운 상태를 생성한다. 이를 통해 코드의 가독성과 유지보수성을 높이고, 불변성을 유지하는 작업에서 발생하는 실수를 줄일 수 있다.
immer를 사용하려면, produce 함수를 사용하여 변경하려는 상태를 전달하고, 업데이트 함수를 작성한다. 업데이트 함수에서는 상태를 변경하는 코드를 작성하면 된다.
immer 사용법 알아보기
immer를 사용해보기 위해 새로운 프로젝트를 생성해주었다.
npx create-react-app immer-tutorial --template typescript
cd immer-tutorial
npm install immer
다음은 immer를 사용해 객체의 프로퍼티를 업데이트하는 예제이다.
import produce from 'immer';
type Person = {
name: string;
age: number;
};
const person: Person = {
name: 'John',
age: 30,
};
const updatedPerson = produce(person, draft => {
draft.age = 31;
});
이 예제에서는 'produce' 함수를 사용해 'person' 객체를 전달하고, 업데이트 함수를 작성한다. 업데이트 함수에서는 'draft' 매개변수를 사용해 상태를 변경할 수 있다. 이때, 'draft'는 'person' 객체의 복제본이며, 내부적으로 불변성을 유지한다. 따라서 'draft.age = 31' 코드를 작성하면, 새로운 객체를 생성하고 'age' 프로퍼티만 변경된 'updatedPerson' 객체가 반환된다.
immer를 사용하지 않고 불변성 유지하기
먼저. immer를 사용하지 않고 불변성을 유지하는 코드를 작성해보자.
import React, { useRef, useCallback, useState } from "react";
function App() {
const nextId = useRef(1);
const [form, setForm] = useState({ name: "", username: "" });
const [data, setData] = useState<{
array: { id: number; name: string; username: string }[];
uselessValue: null;
}>({
array: [],
uselessValue: null,
});
const onChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setForm({
...form,
[name]: value,
});
},
[form]
);
const onSubmit = useCallback(
(e: React.FormEvent<HTMLFormElement>) => {
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: number) => {
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;
이 코드는 입력 폼과 목록을 구현하는 간단한 애플리케이션으로, App 컴포넌트 안에는 nextId, form, data라는 세 가지 상태가 정의되어 있다. nextId는 사용자가 입력한 정보들을 식별하기 위한 고유한 ID 값으로, useRef를 사용해 컴포넌트가 다시 렌더링되어도 유지된다. form은 사용자가 입력한 이름과 아이디 값을 저장하는 객체이다. data는 사용자가 입력한 정보들을 모두 저장하는 배열과 불필요한 값을 가지는 객체이다.
이 코드를 렌더링 해보면, 아이디/이름을 입력했을 때 하단 리스트에 추가되고, 리스트 항목을 클릭하면 삭제되는 컴포넌트를 만들었다. 위처럼 전개 연산자와 배열 내장 함수를 사용해 불변성을 유지하면 상태가 복잡해질 경우 작업이 좀 더 어려워 질 수 있다.
immer 사용
immer를 사용하면, 기존 객체를 직접 수정하는 대신, 객체의 복사본을 만들어 변경 작업을 수행하고, 이를 기존 객체에 적용하는 방식으로 객체를 업데이트할 수 있다. 이렇게 하면 코드의 가독성이 향상되며, 객체의 복잡한 구조를 다룰 때도 보다 간편하게 작업할 수 있다.
immer에서 가장 중요한 함수는 produce이다. 이 함수는 불변성을 유지하면서 상태를 업데이트 할 수 있도록 도와준다. 이 함수는 새로운 상태를 생성하는 함수로, 이전 상태를 인자로 받아 다음 상태를 반환한다. 반환된 상태는 이전 상태와는 다른 새로운 객체이다.
function produce<T>(baseState: T, recipe: (draftState: T) => void | T): T;
import produce from "immer";
const nextState = produce(previousState, draftState => {
// draftState를 이용하여 상태를 업데이트합니다.
});
produce 함수는 위와 같은 타입 시그니처를 가진다. T는 상태의 타입을 나타내며, baseState는 변경하고자 하는 상태 객체를 의미한다. recipe는 변경할 로직을 담은 함수로, draftState 파라미터를 받아 로직을 실행한 후 새로운 상태 객체를 반환하거나, void를 반환해 draftState 자체를 변경할 수 있다.
produce 함수는 이전 상태를 변경하지 않고, 새로운 상태 객체를 생성해 변경 내용을 적용한다. 이 때, 변경 내용이 적용된 새로운 상태 객체는 이전 상태 객체와 타입이 동일하다. 따라서 타입 안정성을 유지하면서 상태를 변경할 수 있다.
immer 라이브러리는 불변성에 신경쓰지 않는 것처럼 코드를 작성하지만 불변성 관리는 제대로 해 주는 것을 목표로 하기 때문에, 단순하게 깊은 곳에 위치하는 값을 바꾸는 것 외에 배열을 처리할 때도 매우 쉽고 편하게 사용할 수 있다.
이전에 작성한 불변성을 유지하는 코드에 produce 함수를 사용한 것으로 고쳐보자.
import React, { useRef, useCallback, useState } from "react";
import produce from "immer";
interface Form {
name: string;
username: string;
[key: string]: string;
}
interface Info {
id: number;
name: string;
username: string;
}
interface Data {
array: Info[];
uselessValue: null;
}
function App() {
const nextId = useRef(1);
const [form, setForm] = useState<Form>({ name: "", username: "" });
const [data, setData] = useState<Data>({ array: [], uselessValue: null });
const onChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setForm(
produce((draft: Form) => {
draft[name] = value;
})
);
}, []);
const onSubmit = useCallback(
(e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const info: Info = {
id: nextId.current,
name: form.name,
username: form.username,
};
setData(
produce((draft: Data) => {
draft.array.push(info);
})
);
setForm({ name: "", username: "" });
nextId.current += 1;
},
[form.name, form.username]
);
const onRemove = useCallback((id: number) => {
setData(
produce((draft: Data) => {
draft.array = draft.array.filter((info) => info.id !== id);
})
);
}, []);
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를 사용해 컴포넌트 상태를 작성할 때는 객체 안에 있는 값을 직접 수정하거나, 배열에 직접적인 변화를 일으키는 push, splice 등의 함수를 사용해도 무방하다. 그렇기 때문에 불변성 유지에 익숙하지 않아도 자바스크립트에 익숙하다면 컴포넌트 상태에 원하는 변화를 쉽게 반영시킬 수 있다.
'WEB > React' 카테고리의 다른 글
리액트 외부 API 연동 실습 [react/API/typescript] (0) | 2023.03.20 |
---|---|
리액트 라우터 써보기 [react/router/typescript] (0) | 2023.03.19 |
리액트 컴포넌트 성능 최적화 [react/component/typescript] (0) | 2023.03.18 |
리액트 일정관리 웹 만들기 [react/typescript] (0) | 2023.03.17 |
리액트 CSS 사용하기 [react/css/typescript] (0) | 2023.03.17 |