immer 내부 살펴보기
이 글은 기본적으로 immer에 대해서 알아보는 시간을 가진다. 만약 immer를 잘 모르는 분들은 아래 챕터를 먼저 읽어보는 것을 권장한다.
무엇이 궁금할까?
Q1. immer
는 객체 mutable하게 바꾸는 방식을 어떻게 immutable한 방식으로 바꾸어주고 있을까?
immer는 mutable하게 변경하는 객체 built-in method를 사용하더라도 immutable하게 데이터를 반환해주는 기능을 한다. 이 기능이 내부적으로 어떤 방식으로 이루어지는지 알아본다.
아래 코드 예시는 immer 공식 문서에 존재하는 basic example을 가져온 것이다.
import produce from 'immer';
const baseState = [
{
title: "Learn TypeScript",
done: true,
},
{
title: "Try Immer",
done: false,
},
]
const nextState = produce(baseState, (draft) => {
draft.push({ title: "Tweet about It" });
draft[1].done = true;
})
console.log(baseState === nextState) // false
console.log(nextState)
/*
[
{
title: "Learn TypeScript",
done: true,
},
{
title: "Learn TypeScript",
done: true,
},
{
title: "Tweet about It",
},
]
*/
Q2. immer
는 어떻게 structural sharing을 사용하는걸까?
*structural sharing
: 객체를 copy할 때 변경되지 않은 객체는 reference를 동일하게 사용하는 방식.
객체를 immutable하게 업데이트 한다는 것은 기존 객체를 새로운 객체로 복사한다는 것이다. 즉, 복사에 비용이 발생한다. immer는 객체를 복사할 때, 변경되지 않은 reference는 재사용하는 structural sharing 방식을 사용해서 객체를 복사한다. immer에서는 어떤방식을 사용해서 structural sharing을 사용하고 있는지 알아본다.
Q3. immer에서는 produce함수 내에서 draft를 직접 업데이트하는 방식이 아니라 return을 통해서 데이터를 업데이트하는 경우가 있는데, 이런 경우에 로직이 다른지?
immer를 사용할 때, 위에서 제시한 mutable한 객체 변경 방식이 아닌, 새로운 객체를 리턴하는 경우가 있다. 이는 immer와는 무관하게 immutable하게 Javascript에서 객체를 반환해주는 방식과 동일하다. immer에서는 이러한 방식을 공식적으로 허용하고 있고 두 방식, mutable하게 객체를 변경하는 방식과 immutable하게 객체를 변경시키는 방식 모두 혼용해서 쓰는 개발자도 많을 것 이다. 이러한 방식 차이는 immer에서 어떤 로직차이를 발생시키는지 알아본다.
// mutable method
const nextState = produce(baseState, (draft) => {
draft.push({ title: "Tweet about It" });
draft[1].done = true;
})
// immutable method
const nextState = produce(baseState, (draft) => {
return {
...baseState,
{ ...baseState[1], done: true },
{ title: "Tweet about It" },
}
})
- immer 혹은 redux-toolkit을 사용해 본 경험
- Proxy에 대한 이해 (optional)
Immer는 무엇이고, 왜 사용하는걸까?
immer를 왜 사용하는지 잘 이해하고 있다면 지루한 이야기가 될 수 있다. 알고 있다면 다음 챕터로 넘어가자.
immer란 무엇인가? immer 공식 문서에서의 소개 문구를 가져와보자.
Immer
(German for: always) is a tiny package that allows you to work with immutable state in a more convenient way.
immer는 javascript에서 data가 immutable하게 업데이트 되는 것을 보장해주는 라이브러리이다.
그렇다면 immer는 어디서 쓰이고 있을까?
redux의 style guide에서는 redux-toolkit을 사용하는 것을 권장하고 있고 immutable data 관리를 위해서는
immer를 사용하는 것이 좋다고 권장하고 있다.
물론 redux-toolkit에는 immer를 사용하고 있으므로 redux-toolkit을
사용하고 있다면 이미 redux에 immer를 사용하고 있는 것이다.
왜 immutable data를 사용해야하는가는 redux의 FAQ항목을 참고하는 것이 좋다.
내용을 간략하게 설명하자면, 다음과 같다.
javascript에서는 primitive한 타입의 변수(number, string, etc)를 제외하면 모두 mutable한 속성을 가진다.
non-primitive한 타입은 object, array와 같은 것들이 있다.
non-primitive한 타입의 변수는 변경되어도 변수의 reference가 바뀌지 않는다. 따라서 object 내부가 변경되더라도 reference가 변경되지 않는 것이다.
let primitive = 5;
let primitive2 = primitive;
console.log(primitive === primitive2) // true
primitive2 = 10;
console.log(primitive === primitive2) // false
let nonPrimitive = { a: 5 };
let nonPrimitive2 = nonPrimitive;
console.log(nonPrimitive === nonPrimitive2) // true
nonPrimitive2.b = 10;
console.log(nonPrimitive === nonPrimitive2) // true
console.log(nonPrimitive)
// { a: 5, b: 10 }
redux에서는 shallow equality checking을 사용하고 있다. shallow equality checking은 데이터가 동일한지 비교할 때, 데이터 내부가 변경되었는지를 확인하는 것이 아니라 데이터의 reference가 변경되었는지만 체크를 하고 동일하면 변경되지 않았다고 판단하는 것이다.
만약 deep equality checking을 사용하면 모든 객체를 하나씩 비교해야하기 때문에 성능상 손해를 보게 된다. 그래서 object값을 변경할 때 reference도 변경되는 것을 보장하는 immutable data를 사용하게 되었고 어떤 변경이더라도 객체가 immutable함을 보장해주는 immer를 사용하게 되는 것이다.
만약 따로 immer를 의식적으로 사용한 적이 없더라도, redux를 사용하는데 redux-toolkit을 사용하고 있다면 이미 immer를 사용하고 있는 것이다.
사용법
immer의 내부를 확인하기 전에 immer를 어떻게 사용하는지 한번 확인해보자. 다음은 immer docs에 나와 있는 예제이다.
- baseState를 immutable하게 변경하는 방법에 대한 비교.
const baseState = [
{
title: "Learn TypeScript",
done: true
},
{
title: "Try Immer",
done: false
}
]