Skip to content

Immer.js 도입기

KingDonggyu edited this page Dec 7, 2022 · 6 revisions

작성자: 김동규


현재 우리 팀은 Zusthand를 이용하여 상태 관리를 수행하고 있다.

Zusthand는 FLUX 패턴을 따른다.

FLUX 패턴?

액션이 발생하면 해당하는 방법으로 상태를 변화시키고 이를 view에 반영하는 단방향 데이터 흐름 구조를 말한다.

이 과정에서 side effect를 없애기 위해 새로운 상태는 이전 상태와 완전히 다른 새로운 객체가 되어야 한다.

또한 React는 상태가 변경하게 되면 리렌더링이 발생하는데, 이전 상태 값과 변경된 상태 값이 같다면 리렌더링이 일어나지 않는다.


따라서! 불변성이 보장되어야 한다!

아래는 내가 실제로 다루었던 상태 데이터 구조이다.

{
  "userId": number,
  "exerciseList": [
    {
      "routineId": number,
      "exerciseName": "string",
      "setList": [
        {
          "kg": number,
          "count": number,
          "check": number,
        }
      ]
    }
  ]
}

아래는 위 데이터 구조의 상태를 불변성이 보장되도록 한 Zusthand 코드이다.

const exerciseStore = create<ExerciseState>((set) => ({
  exerciseList: [getInitialExercise()],

  initExerciseList: () => set({ exerciseList: [getInitialExercise()] }),

  createExerciseItem: () => {
    set(({ exerciseList }) => ({ exerciseList: [...exerciseList, getInitialExercise()] }));
  },

  deleteExerciseItem: (exerciseId: number) => {
    set(({ exerciseList }) => ({
      exerciseList: updateOneElementFromArray(exerciseList, exerciseId),
    }));
  },

  createExerciseSetItem: (exerciseId: number) => {
    set(({ exerciseList }) => ({
      exerciseList: updateOneElementFromArray(exerciseList, exerciseId, {
        ...exerciseList[exerciseId],
        setList: [
          ...exerciseList[exerciseId].setList,
          { ...exerciseList[exerciseId].setList[exerciseList[exerciseId].setList.length - 1] },
        ],
      }),
    }));
  },

  deleteExerciseSetItem: (exerciseId: number) => {
    set(({ exerciseList }) => ({
      exerciseList: updateOneElementFromArray(exerciseList, exerciseId, {
        ...exerciseList[exerciseId],
        setList: [
          ...exerciseList[exerciseId].setList.slice(0, exerciseList[exerciseId].setList.length - 1),
        ],
      }),
    }));
  },

  updateExerciseName: (exerciseId: number, exerciseName: string) => {
    set(({ exerciseList }) => ({
      exerciseList: updateOneElementFromArray(exerciseList, exerciseId, {
        ...exerciseList[exerciseId],
        exerciseName,
      }),
    }));
  },

  updateExerciseSetList: (exerciseId: number, setId: number, setItem: ExerciseSet) => {
    set(({ exerciseList }) => ({
      exerciseList: updateOneElementFromArray(exerciseList, exerciseId, {
        ...exerciseList[exerciseId],
        setList: updateOneElementFromArray(exerciseList[exerciseId].setList, setId, {
          ...setItem,
        }),
      }),
    }));
  },

  fetchRoutine: (routineInfo: RoutineDetailInfo[]) => {
    set({
      exerciseList: routineInfo.map(({ exerciseName, set: setList }) => ({
        exerciseName,
        setList: setList.map(({ kg, count }) => ({
          kg,
          count,
          check: 0,
        })),
      })),
    });
  },
}));

끔찍하다.. 사실 이 것보다 더 코드가 복잡했다.

위 코드는 불변성을 보장하도록 배열을 업데이트하는 updateOneElementFromArray 유틸 함수를 새로이 생성하고 적용한 결과이다.


그리고 또, 한가지 문제가 발생했다.

현재 프로젝트에서 React Query를 사용하고 있다.

이때, 나의 모든 운동 기록 날짜를 서버에 요청하면 아래와 같이 날짜 문자열 배열이 응답온다.

image

위 이미지에서 보이듯 2022년 12월 8일 운동 기록 내역이 있는데..

image

잔디에 운동 표시가 하나도 없다..!

image

캘린더에도...😱

알아본 결과, React Query의 useQuery Hook을 통해 받아온 데이터에 대해 불변성을 고려하지 않고 그대로 조작했기에,

(신나게 shift()를 했다)

해당 데이터가 빈 배열이 됨으로써 잔디와 캘린더에 아무 운동 기록이 찍히지 않게 된 것이다.


클라이언트 상태 뿐 아니라, 이와 같이 서버 데이터 또한 불변성을 보장해줘야 한다.

하지만 서버에서 반환하는 데이터마다 구조가 일정하지 않을 뿐더러, 복잡한 경우도 많다.

따라서 클라이언트의 상태와 서버 데이터에 대해 불변성 보장을 조금 더 직관적이고 편리하게 할 필요성을 느꼈다.


그래서! Immer.js를 도입하게 되었다.

Immer.js를 사용하는 방법은 간단하다.

설치한 immer 모듈로부터 produceimport하고, 불변성을 보장하기 위한 객체를 어떻게 조작할지 작성한 콜백 함수를 전달해주면 된다.


Immer.js를 적용해서 아까 위에 Zusthand 코드를 변경해보자!

const exerciseStore = create<ExerciseState>((set) => ({
  exerciseList: [getInitialExercise()],

  initExerciseList: () => set({ exerciseList: [getInitialExercise()] }),

  createExerciseItem: () => {
    set(
      produce(({ exerciseList }: ExerciseState) => {
        exerciseList.push(getInitialExercise());
      }),
    );
  },

  deleteExerciseItem: (exerciseId: number) => {
    set(
      produce(({ exerciseList }: ExerciseState) => {
        exerciseList.splice(exerciseId, 1);
      }),
    );
  },

  createExerciseSetItem: (exerciseId: number) => {
    set(
      produce(({ exerciseList }: ExerciseState) => {
        const { setList } = exerciseList[exerciseId];
        setList.push(setList[setList.length - 1]);
      }),
    );
  },

  deleteExerciseSetItem: (exerciseId: number) => {
    set(
      produce(({ exerciseList }: ExerciseState) => {
        exerciseList[exerciseId].setList.pop();
      }),
    );
  },

  updateExerciseName: (exerciseId: number, exerciseName: string) => {
    set(
      produce(({ exerciseList }: ExerciseState) => {
        exerciseList[exerciseId].exerciseName = exerciseName;
      }),
    );
  },

  updateExerciseSetList: (exerciseId: number, setId: number, setItem: ExerciseSet) => {
    set(
      produce(({ exerciseList }: ExerciseState) => {
        exerciseList[exerciseId].setList[setId] = setItem;
      }),
    );
  },

  fetchRoutine: (routineInfo: RoutineDetailInfo[]) => {
    set(
      produce((draft: ExerciseState) => {
        draft.exerciseList = routineInfo.map(({ exerciseName, set: setList }) => ({
          exerciseName,
          setList: setList.map(({ kg, count }) => ({
            kg,
            count,
            check: 0,
          })),
        }));
      }),
    );
  },
}));

훨~씬 편리하게 불변성을 보장할 수 있고 코드 가독성 또한 좋아졌다!

이처럼 React Query의 useQuery 반환 데이터 또한 적용했다.


하지만! 주의해야 할 점이 있다 ❗️❗️

Immer.js 를 사용하지 않은 코드가 조금 더 빠르다.

따라서 모든 상태와 서버 데이터 적용하는 것은 비효율적이라 판단했다.

그래서 정말로 복잡한 상태를 다루는 경우만 사용하기로 했다.


React Query의 useQuery가 반환하는 데이터에 적용한 Immer.js 코드를 다시 제거했고, as const를 붙여 읽기 전용으로 변경했다.

단, 해당 데이터를 조작해야 하고 동시에 데이터 구조가 복잡할 경우에만 Immer.js를 적용하자.


결론으로 복잡한 데이터 구조의 상태와 서버 데이터를 관리할 때 불변성을 크게 신경쓰지 않게 되었다.

라이브러리 자체가 가볍기 때문에 Immer.js를 도입한 선택에 만족하고 있다.

💻 Projects

🤝 Rules

🎙️ Meeting

👾 Trouble Shootings

🛠 Tech Semina

🔰 초심자를 위한 기술 가이드

🏃‍♂️ Sprint

✏️ Reviews

💎 Mentoring

💬 Scrums

Week 1
Week 2
Week 3
Week 4
Week 5
Week 6
Clone this wiki locally