IT/React & Next.js

[React] 불변성과 immer 라이브러리

땅일단 2024. 9. 26. 22:52

(이번 포스팅 예시코드는 TypeScript로 작성함)

 

React에서 상태를 업데이트할 때, 기존의 값을 유지하면서 상태값을 변경하는 것은 중요한데, 이를 불변성(Immutability)이라고 한다.

 

const [nums, setNums] = useState<number[]>([1, 2, 3]);

useEffect(() => {
    console.log(nums);
}, [nums]);

const addNum = () => {
    nums.push(4);
}

 

예를 들어, 위와 같이 리스트에 push를 했을 땐 값이 변경되었지만 useEffect에서 감지하지 못한다.

 

 

const addNum = () => {
    nums.push(4);
    setNums([...nums, 5]);    // [1, 2, 3, 4, 5]
}

 

그래서 위와 같이 스프레드 연산자를 통해 리스트를 복사하고, 추가하고 싶은 값을 넣어줌으로써 새 리스트를 만든 후 setState로 상태를 변경해야 한다. 

 

물론 리스트 뿐만 아니라 객체도 위와 같이 상태를 변경해야 한다.

 

하지만 스프레드 연산자는 저번 ES6 포스팅에서도 다루었듯이 중첩된 리스트나 객체에서는 한 단계만 깊은 복사가 되고 그보다 깊은 레벨은 얕은 복사가 된다.

 

 

아래 코드를 보자.

const [userInfo, setUserInfo] = useState<UserInfo>({id: "doringri", detail: {age: 5}});

useEffect(() => {
    console.log(userInfo);    // {id: "admin", detail: {age: 7}}
}, [userInfo]);

const changeUserInfo = () => {
    const newUserInfo: UserInfo = {id: "admin", detail: {age: 6}};
    setUserInfo(newUserInfo);

    newUserInfo.detail.age = 7;
}

 

newUserInfo의 값만 변경했는데 useEffect 블록 안에서 userInfo를 찍어 보면 상태값까지 변경되었다... ㄷㄷ

userInfo.detail.job은 newUserInfo.detail.job의 메모리를 계속 참조하고 있는 것이다.

 

 

 

결론은 객체가 복잡해지면 스프레드 연산자만으로는 불변성을 유지하기 어려워진다는 문제점이 있고, 상태를 그대로 복사한다는 점에서 코드 가독성이 좋지 않기도 하다.

 

이런 문제점을 immer 라이브러리로 쉽게 해결할 수 있다.

 

(대충 인기 많다는 뜻)

 

npm i -D immer

 

설치해주고 사용해보자.

 

import { produce, Draft } from "immer";

const [userInfo, setUserInfo] = useState<UserInfo>({id: "doringri", detail: {age: 5}});

// immer 사용하지 않음
const changeUserInfo = () => {
    setUserInfo({...userInfo, detail: {age: userInfo.detail.age + 1}})
}

// immer 사용함
const changeUserInfoWithImmer = () => {        
    setUserInfo(produce(userInfo, (draft: Draft<UserInfo>) => {
        draft.detail.age += 1;
    }));
}

 

immer에서 제공하는 produce 함수를 통해 매우 직관적인 코드로 바뀌었다.

이때 유의해야 할 점은 draft의 내부 속성을 변경해야 useEffect 등에서 상태가 변경됐음을 감지한다는 것이다.

 

 

const changeUserInfoWithImmer2 = () => {        
    const newUserInfo: UserInfo = {id: "admin", detail: {age: 6}};

    setUserInfo(produce(userInfo, (draft: Draft<UserInfo>) => {
        draft = newUserInfo;
    }));
}

 

예시로, 위 코드처럼 draft에 객체를 그대로 할당시키는 건 인식을 못한다.

 

 

const changeUserInfoWithImmer2 = () => {        
    setUserInfo(produce(userInfo, (draft: Draft<UserInfo>) => {
        draft.id = "admin";
        draft.detail.age = 6;
    }));
}

 

위처럼 내부 속성을 직접 변경해주면 useEffect에서 변경을 감지하는 것을 볼 수 있다.