728x90

다이시 카토가 만든 전역 상태 관리 라이브러리 중 하나인 Jotai에서는 WeakMap을 통해서 atom을 관리합니다. 이에 WeakMap이 무엇이고 왜 사용했는지에 대해 알아보려고 합니다.

 

우선 조타이 라이브러리의 공식 docs에서 언급한 왜 WeakMap을 사용했는지 입니다.

Let's start with an easy example. An atom is just a function that will return a configuration object. We are using WeakMap to map atom with their state.
WeakMap doesn't keep its keys in memory, so if an atom is garbage collected, its state will be garbage collected too. This helps avoid memory leaks.

 

요약하면 아래와 같습니다.

1. 조타이에서는 atom으로 상태관리를 하는데 atom은 함수이다.

2. WeakMap은 키를 메모리에 유지하지 않기 때문에 가비지 콜렉터가 atom을 수집하면 상태도 제거해준다.

 

즉, 손쉽게 메모리 누수를 방지하기 위해서 조타이에서는 WeakMap을 사용합니다. 그렇다면 왜 WeakMap은 무엇일까요?

 

WeakMap에 대해서

WeakMap은 이름부터 JS의 Map과 관계가 있어보입니다. 따라서 우선 Map에 대해서 알아보려고 합니다.

 

MDN문서에 설명하는 Map은 아래와 같습니다.

ECMAScript 6에서 값들을 매핑하기 위한 새로운 데이터 구조를 소개 하고 있다. 그중 하나인 Map객체는 간단한 키와 값을 서로 연결(매핑)시켜 저장하며 저장된 순서대로 각 요소들을 반복적으로 접근할 수 있도록 한다.

 

Object와 Map 비교
1. Object의 키는 Strings이며, Map의 키는 모든 값을 가질 수 있다.
2. Object는 크기를 수동으로 추적해야하지만, Map은 크기를 쉽게 얻을 수 있다.
3. Map은 삽입된 순서대로 반복된다.
4. 객체(Object)에는 prototype이 있어 Map에 기본 키들이 있다. (이것은 map = Object.create(null) 를 사용하여 우회할 수 있다. )

 

자바스크립트에서는 key,value가 쌍으로 이루어진 자료구조에는 Object, Map, WeakMap 3가지가 중요합니다.

앞서 말했듯이 조타이는 atom이라는 함수를 키로 상태를 관리합니다. 그리고 함수는 객체이기에, Map이나 WeakMap이 적합합니다. 

 

그렇다면  WeakMap은 어떤 차이점이 있을까요?

Mdn에서 말하는 WeakMap의 장점은 아래와 같습니다.

  • 가비지 컬렉션을 방지하지 않으므로 키 객체에 대한 참조가 결국 사라집니다.
  • 키 객체가 WeakMap 밖의 다른 곳에서 참조되지 않으면 그 값의 가비지 컬렉션도 허용합니다.

그리고 자바스크립트 엔진의 가비지 컬렉터의 특징을 아래 예시를 통해 확인해보고자 합니다.

자바스크립트 엔진은 도달 가능한 (그리고 추후 사용될 가능성이 있는) 값을 메모리에 유지합니다.
출처 : https://ko.javascript.info/garbage-collection
let object = { name: 'Garbage' };

let collector = [object];

object = null; // 참조를 null로 덮어씀

console.log(JSON.stringify(collector[0])); // {"name":"Garbage"}

let obj2 = { name: 'Trash' };

let weakMap = new WeakMap();

weakMap.set(obj2, 'die');

obj2 = null; // 참조를 덮어씀

console.log(typeof weakMap.get(obj2)); //undefined

따라서 아래의 예시를 통해서 값을 확인해보면, 첫 번째 예시의 object의 경우 collector에 의해 참조되고 있기 때문에 도달할 수 있다고 평가되어 지고 값이 유지되어 있음을 볼 수 있습니다. 하지만 weakMap의 키로 사용된 obj2의 경우에는 참조로 덮어씌워 졌을 때, 가비지 컬렉터에 의해 수거됩니다. 그리고 WeakMap에서 키가 수거된 경우에는 자연스럽게 값도 같이 가비지 컬렉터에 의해 수거됐음을 확인할 수 있습니다.

 

 Jotai에서 WeakMap

다이시 카토에 의하면 Jotai도 구독 모델 기반의 라이브러리입니다.

아래는  Jotai의 createStore입니다. WeakMap을 사용하는 것을 확인할 수 있네요.

export const createStore = (): Store => {
  const atomStateMap = new WeakMap()
  const getAtomState = <Value>(atom: Atom<Value>) => {
    let atomState = atomStateMap.get(atom) as AtomState<Value> | undefined
    if (!atomState) {
      atomState = { d: new Map(), p: new Set(), n: 0 }
      atomStateMap.set(atom, atomState)
    }
    return atomState
  }
  return buildStore(getAtomState)
}

 

이 소스코드에서 알 수 있듯이 WeakMap을 통해서 아톰의 상태를 저장하고, 이미 존재하는 값이라면 해동 값을 가져옵니다.

 

이를 통해서 만약 React에서 Jotai를 통해 상태관리를 할 경우에, 해당 컴퍼넌트가 unmount되어 더 이상 atom이 참조되고 있지 않다면, 가비지 컬렉터에 의해 atom이 수거되어 메모리 누수를 방지하는 장점을 얻습니다.

 

 

 

 

 

참고 문헌 및 출처

리액트 훅을 활용한 마이크로 상태 관리 - 다이시 카토 지음

 

https://ko.javascript.info/weakmap-weakset

 

위크맵과 위크셋

 

ko.javascript.info

 

https://ko.javascript.info/garbage-collection

 

가비지 컬렉션

 

ko.javascript.info

 

https://developer.mozilla.org/ko/docs/Web/JavaScript/Guide/Keyed_collections

 

키 기반 컬렉션 - JavaScript | MDN

이번 장에서는 입력된 키값을 기준으로 정렬되는 데이터의 집합(자료 구조)에 대해 소개 할 것이다. Map과 Set은 입력된 순서대로 반복적으로 접근 가능한 요소들을 포함하고 있다.

developer.mozilla.org

 

 

728x90

Zustand는 상태를 유지하는 store를 만드는데 사용되는 라이브러리입니다. 

import { create } from 'zustand';

type StoreState = {
  count: number;
  text: string;
  inc1: () => void;
};

export const useEx1Store = create<StoreState>((set) => ({
  count: 0,
  text: 'hello',
  inc1: () => set((prev) => ({ count: prev.count + 1 })),
}));

 

이때 위의 예시에서 반환되는 값 useEx1Store의 타입은 아래와 같습니다.

export interface StoreApi<T> {
    setState: SetStateInternal<T>;
    getState: () => T;
    getInitialState: () => T;
    subscribe: (listener: (state: T, prevState: T) => void) => () => void;
}

 

setState는 불변성을 활용하여 값을 설정하거나 이전 값을 활용하여 갱신이 가능합니다.  setState의 경우에는 내부적으로 Object.assign으로 구현되어 있으며 , 그렇기에 store에서 이전 상태와 새 상태를 병합하여 새 객체를 반환합니다.

getState와 getInitialstate로 상태 및 초기 상태를 얻을 수 있습니다.

마지막으로 subscribe를 통해서 구독을 하여, 상태가 변경되었을 때 콜백함수가 실행되게 할 수 있습니다. 이러한 구독을 활용하여, 리엑트에서 상태를 공유하는 다른 컴퍼넌트들이 리렌더링하도록 할 수 있습니다.

 

선택자함수를 사용하여 수동 렌더링 최적화 하기

import { create } from 'zustand'

const useStore = create((set) => ({
  count: 1,
  inc: () => set((state) => ({ count: state.count + 1 })),
}))

function Counter() {
  const { count, inc } = useStore()
  return (
    <div>
      <span>{count}</span>
      <button onClick={inc}>one up</button>
    </div>
  )
}

 

아래 코드는 zustand의 공식페이지에 나와 있는 예시 입니다. 보통 이 예시를 보고 zustand의 간단한 사용법에 혹해서 도입하는 경우가 많다고 생각합니다. 하지만 지금은 상태가 단 하나뿐이라서 큰 문제가 없지만,  만약에 처음에 보였던 예시처럼 count와 text 두 가지 상태를 공유하고 count만 변경하는 경우에 문제가 발생합니다. 이를 확인하기 위해서 아래와 같은 예시를 만들었습니다.

 

각각의 컴퍼넌트를 렌더링하는 앱

import './App.css';
import { CountChanger, ZustandEx1, ZustandEx2 } from './test/chp7/ZustandEx1';

function App() {
  return (
    <>
      <ZustandEx1 />
      <ZustandEx2 />
      <CountChanger />
    </>
  );
}

export default App;

 

선택자 사용 유무 리렌더링을 비교하기 위해서 분리함

import { useEx1Store } from './count';

export const ZustandEx1 = () => {
  const { text } = useEx1Store();
  console.log('리랜더링 체크 1');
  return (
    <>
      <div>{text}</div>
    </>
  );
};

export const ZustandEx2 = () => {
  const text = useEx1Store((state) => state.text);
  console.log('리랜더링 체크 2');
  return (
    <>
      <div>{text}</div>
    </>
  );
};

export const CountChanger = () => {
  const inc1 = useEx1Store((state) => state.inc1);
  console.log('리렌더링함?');
  return <button onClick={inc1}>Zustand Count 증가</button>;
};

 

count.ts ( zustand 코드)

import { create } from 'zustand';

type StoreState = {
  count: number;
  text: string;
  inc1: () => void;
};

export const useEx1Store = create<StoreState>((set) => ({
  count: 0,
  text: 'hello',
  inc1: () => set((prev) => ({ count: prev.count + 1 })),
}));

 

예시 페이지

 

count 값을 증가하는 함수가 존재하고, 전역 store에는 count와 text가 존재합니다.

이때 CountChanger는 단순히 count 값을 증가시키는 store 내부의 함수이고, ZustandEx1에서는 선택자 없이 text를 사용 Ex2에서는 선택자를 활용하여 상태 증가를 시키고 있습니다.

그리고 버튼을 누르게 되면 콘솔을 통해서 리렌더링 횟수를 확인할 수 있습니다.

 

초기에 렌더링 될 때 3가지의 콘솔이 모두 찍히고, 이후 버튼 클릭시에 count 와 전혀 무관한 것 같은 ZustandEx1이 리렌더링 됨을 확인할 수 있습니다. 이는 store를 구독하고 있기 때문에, store 상태가 변경될 때마다 리렌더링 되기 때문입니다.

 

이를 해결하기 위한 방법이 선택자 함수 입니다.

  const text = useEx1Store((state) => state.text);

store 내부에 있는 상태 중에서 사용하고 싶은 상태를 선택하게 되면 해당 값을 제외한 상태 변경에는 store를 구독했어도 리렌더링 되지 않습니다.

이러한 것을 리액트 훅을 사용한 마이크로 상태 관리 책에서는 수동 렌더링 최적화 라고 부릅니다.

 

이를 활용하면 파생 상태에 대한 리렌더링도 최적화 할 수 있습니다. 만약 count 값 2개를 합친 값의 최적화를 하는 경우에는 1 + 2 는 3입니다. 하지만 만약 값이 변경되어 0 + 3 이 되어도 파생 값은 변경되지 않았기에 리렌더링할 필요가 없습니다. 이때 아래와 같이 선택자에서 파생 값(지금은 count 와 count2의 합)을 선택하면 리렌더링이 최적화 됩니다.

  const text = useEx1Store((state) => state.count + state.count2);

 

 

교재에서는 Zustand의 장점으로는 리액트와 동일한 모델을 사용해 라이브러리의 단순성과 번들 크기가 작다는 점을 꼽고 있고, 단점으로는 선택자를 이용한 수동 렌더링 최적화로 꼽고 있습니다. 객체 참조 동등성을 이해해야 하며, 선택자 코드를 위해 보일러플레이트 코드를 많이 작성해야 한다고 합니다. 

 

참고문헌

https://zustand-demo.pmnd.rs/

 

Zustand

 

zustand-demo.pmnd.rs

공식 소스 코드

 

리액트 훅을 활용한 마이크로 상태 관리 (다이시 카토 지음)

 

 

728x90

준비하게 된 계기

 

프론트엔드를 공부하다 정보처리기사를 준비하게 되었습니다. 

정처기를 준비하게 된 이유는 비전공자로서 프론트엔드에 대해 공부할 때, 생각보다 모르는 용어가 많다는 느낌을 받았기 때문입니다. 물론 모르는게 생길때마다 이를 공부할 수도 있지만, 디자인 패턴등을 공부하면서 느낀점이 몰랐고 겪지 않았기 때문에 불편함을 느끼지 못했고, 개선하지 못하는 경우가 많다는 것이었습니다. 따라서 전반적인 소프트웨어적인 지식을 갈고 닦고자 정보처리기사 시험을 준비하게 되었습니다.

또한 이를 바탕으로 비전공자에서 오는 일종의 컴플렉스 또한 극복하고 싶었습니다.

준비하며

사실 시험을 준비하면서 좋았던  점도 많고, 제 생각과 다른점도 많았습니다. 면접 질문으로서만 공부했던 TCP/IP와 UDP에 대해서 조금은 더 자세히 알 수 있었고, metadata등 막연하게 사용하던 용어에 대해서도 좀 더 정확하게 알 수 있었습니다. 주로 사용하던 패턴이외에 다양한 디자인 패턴에 대해서도 맛 볼 수 있었습니다.

물론 해당 내용들이 깊지는 않았지만 이러한 것들이 있고, 이 용어들이 여기서 유래되었구나 하는 사실들을 알 수 있었습니다.

 

아쉬운 점은 시험 자체는 합격을 기준으로 봤을 떄, 언어에 많이 치우쳐 있었습니다. 그리고 그 속에서 C, 자바, 파이썬의 비중은 높았지만 자바스크립트의 비중이 굉장히 낮다는 사실을 확인했습니다. 프론트엔드 개발자로서 자바스크립트를 가장 큰 비중에 두고 학습했지만, 소프트웨어쪽에서는 작은 비중을 차지하는 언어구나 라는 생각이 들었습니다.

 

시험 준비 과정

애초에 목적이 소프트웨어적 지식 함양과 비전공자로서의 자격지심을 어느 정도 해결할 수 있는 자격증의 획득 2가지 였습니다. 따라서 인터넷에 나오는 시험준비 기간 (필기 1주 , 실기 2~3주) 보다는 훨씬 넉넉하게 투자하였습니다.

실기 6주, 필기 3주 정도 투자하였고, 문제집은 시나공을 활용하였습니다. 

 

 

1회독을 끝내고, 기출 문제를 푼 후 틀린 단원을 다시 공부하였고 마지막 시험 직전 3일에는 시험에 주로 기출되는 단원 위주로 학습하였습니다.

 

 

시험 결과

남들보다 시간을 좀 더 투자해서인지 시험은 한번에 모두 합격하게 되었습니다.

단순히 시험 합격만 생각한다면 2,4,7,8,9,10,11만 공부해도 되겠다는 생각이 들었습니다. 시험 전날에 유튜브를 통해서 C언어와 자바에 대한 문제 풀이에 대해서 공부했던 것도 많이 도움이 되었고 단답형 문제에서도 아는게 많이 나와서 합격할 수 있었다고 생각합니다.

 

 

 

'후기' 카테고리의 다른 글

fastcampus 3월 캠프콘- 1:1 커피챗 후기  (0) 2024.04.19
패스트캠퍼스 3월 CampCON  (0) 2024.03.26

+ Recent posts