본문 바로가기
꼬리에 꼬리를 무는 코딩

2022.10.03 꼬꼬코 immer 라이브러리는 어떤 원리로 동작하는가?

by 치우치지않는 2022. 10. 3.

https://hmos.dev/deep-dive-to-immer#immer%EB%8A%94-%EB%AC%B4%EC%97%87%EC%9D%B4%EA%B3%A0-%EC%99%9C-%EC%82%AC%EC%9A%A9%ED%95%98%EB%8A%94%EA%B1%B8%EA%B9%8C

 

immer 내부 살펴보기 | hmos.dev

redux에서 주로 사용되는 immer는 mutable한 객체 업데이트를 immutable하게 사용하는 것 처럼 도와준다. 어떻게 이런 로직이 가능한걸까?

hmos.dev

^윗글을 참고하였다! 나도 언젠가 구글링했을 때 처음으로 나오는 유용한 글을 쓰는 개발 블로거가 되고 싶다..!! 

1. 윗글을 읽어 보았는데 완벽하게 이해하려면 꽤나 시간이 걸릴 것 같아 우선 간단하게 정리하고, 추후 지속적으로 내용을 업그레이드할 예정이다. 

2. 먼저 produce 함수의 내부를 뜯어보자. 

export class Immer {
  produce: (base, recipe) => { 
    let result;

    const scope = enterScope(this);
    const proxy = createProxy(this, base, undefined);
    
    result = recipe(proxy);

    return processResult(result, scope);     
  }
}

produce 는 매개변수로 base, recipe 를 받고 있다. 또 proxy 변수에 새로운 Proxy 를 만들어 저장하고, 그 proxy 를 recipe 함수에 전달해 결과를 만든 다음 result 와 scope 를 processResult 함수에 인자로 넣어 리턴한다. 

앞서 살펴 보았듯, base 는 기존의 객체, recipe 는 업데이트 함수이다. scope 는 immer 전역에서 사용할 데이터를 저장하는 변수라고 간단히 알아두자. 

중요한 것은 proxy 이다. 

3. 그럼 createProxy 함수를 뜯어보자.

export function createProxy(immer, value, parent) {
  const draft = createProxyProxy(value, parent);
  const scope = getCurrentScope();
  
  scope.drafts_.push(draft);
  return draft;
}

앗 이런! createProxy 안에 createProxyProxy 라는 함수가 하나 더 있다! 이는 뒤에서 뜯어 보기로 하고, 중요한 건 scope.drafts_.push(draft) 이 부분이다. 생성한 proxy 들은 scope.drafts_ 배열에 push 된다.

4. 다음으로 createProxyProxy 함수를 뜯어보자.

export function createProxyProxy(base, parent) {
  const state = {
    ...
    scope_: getCurrentScope(),
    modified_: false,
    finalized_: false,
    parent_: parent,
    base_: base,
    draft_: null,
    copy_: null,
    ...
  }

  const target = state;
  const traps = objectTraps
  const { revoke, proxy } = Proxy.revocable(target, traps);
  state.draft_ = proxy;
  state.revoke_ = revoke
  
  return proxy;
}

사실상 이 함수가 가장 근본(근_본)이 되는 함수라고 볼 수 있다. immer 를 사용하기 위해 필요한 메타데이터를 저장하고 있으며, 새로운 proxy 객체를 만들어 리턴하고 있기 때문이다.

 

base_: 기존 data. produce에서 첫번째 인자로 들어왔으며 변경되기 이전 원본 데이터를 여기에 저장한다.
copy_: 업데이트 된 data. 원본 데이터와 recipe를 이용해서 업데이트 된 데이터를 여기에 저장한다. 아직은 아무 데이터도 저장되어 있지 않다.
draft_: draft_ 는 여기서 생성되는 Proxy 객체를 저장한다. 앞으로의 로직에서 draft_.base_나 draft_.copy_와 같은 방법으로 데이터를 참조하게 된다.
modified_: 객체가 변경되었는지 여부를 저장한다. 기본 값은 객체가 변경되지 않았으므로 false 이다.
finalized_: proxy가 업데이트가 완료되어 return 될 준비가 되었는지를 저장한다. 기본 값은 객체가 준비 중이므로 false 이다.
parent_: 객체는 multi depth로 구성될 수 있다. 만약 객체가 트리 형태로 구성된다면 부모 객체를 이 곳에 저장하게 된다. root proxy에서는 부모가 없다.

메타 데이터 설명은 윗 블로그에 있는 설명으로 갈음한다! 너무 잘 정리해주신 관계로!

5. 이제 Proxy.revocable 과 traps 를 이용해 새로운 객체를 생성하는 방법만 정리하면 된다! 이는 내일 꼬꼬코 주제로 토스!

댓글