React 바쁜 주말 #14

CoderS·2021년 12월 19일
0

리액트 Remind

목록 보기
14/32

#14 춥다 휴 :<

Immer 를 사용한 더 쉬운 불변성 관리

리액트에서 배열이나 객체를 업데이트 해야 할 때 직접 수정하면 안되고 불변성을 지켜주면서 업데이트를 해줘야한다.

예 )

const object = {
  a: 1,
  b: 2
};

object.b = 3;

객체가 있다면...

연산자를 사용해서 새로운 객체를 만들어줘야한다.

const object = {
  a: 1,
  b: 2
};

const nextObject = {
  ...object,
  b: 3
};

배열도 마찬가지로 push, splice 등의 함수를 사용해서 n번째 항목을 직접 수정하면 안되고 concat, filter, map 등의 함수를 사용해야 한다.

const todos = [
  {
    id: 1,
    text: '할 일 #1',
    done: true
  },
  {
    id: 2
    text: '할 일 #2',
    done: false
  }
];

const inserted = todos.concat({
  id: 3,
  text: '할 일 #3',
  done: false
});

const filtered = todos.filter(todo => todo.id !== 2);

const toggled = todos.map(
  todo => todo.id === 2
    ? {
      ...todo,
      done: !todo.done,
    }
    : todo
);

대부분 경우 배열 내장함수나 연산자를 통해서 어렵지 않게 코드를 작성할 수 있지만, 조금만이라도 데이터가 까다라워지면 불변성을 지켜가면서 새로운 데이터를 생성하는 코드를 작성해야한다.

가령 다음과 같은 객체가 있을 때

const state = {
  posts: [
    {
      id: 1,
      title: '제목입니다.',
      body: '내용입니다.',
      comments: [
        {
          id: 1,
          text: '와 정말 잘 읽었습니다.'
        }
      ]
    },
    {
      id: 2,
      title: '제목입니다.',
      body: '내용입니다.',
      comments: [
        {
          id: 2,
          text: '또 다른 댓글 어쩌고 저쩌고'
        }
      ]
    }
  ],
  selectedId: 1
};

위에서 posts 배열 안의 id 가 1인 객체를 찾아서, comments 에 새로운 댓글 객체를 추가해줘야 한다고했을 때, 우리는 다음과 같이 업데이트를 해줘야한다.

const nextState = {
  ...state,
  posts: state.posts.map(post =>
    post.id === 1
      ? {
          ...post,
          comments: post.comments.concat({
            id: 3,
            text: '새로운 댓글'
          })
        }
      : post
  )
};

어렵지는 않지만 코드 구조가 좀 복잡해서 코드를 봤을 때 한 눈에 들어오질 않는다.

이 때, immer 이라는 라이브러리를 사용해서 구현할 수 있다.

const nextState = produce(state, draft => {
  const post = draft.posts.find(post => post.id === 1);
  post.comments.push({
    id: 3,
    text: '와 정말 쉽다!'
  });
});

전보다 훨씬 깔끔하게 잘 읽혀진다.

Immer 를 사용하면 우리가 상태를 업데이트 할 때, 불변성을 신경쓰지 않으면서 업데이트를 해주면 Immer 가 불변성 관리를 대신 해준다.

Immer 사용법

이번에는 우리각 만든 사용자 관리 프로젝트에 Immer를 적용해서 사용법에 대해 알아보겠다.

우선은 밑에 명령어를 실행하여 Immer 를 설치해준다.

$ yarn add immer

이 라이브러리를 사용할 때 다음과 같이 사용한다.

코드의 상단에 불러와줘야하는데, 보통 produce 라는 이름으로 불러온다.

import produce from 'immer';

produce 함수를 사용할 때, 첫 번째 파라미터로 수정하고 싶은 상태, 두 번째 파라미터에는 어떻게 업데이트하고 싶을지 정의하는 함수를 적어준다.

두번째 파라미터에 넣는 함수에서는 불변성에 대해서 신경쓰지 않고 그냥 업데이트 해주면 다 알아서 해준다.

const state = {
  number: 1,
  dontChangeMe: 2
};

const nextState = produce(state, draft => {
  draft.number += 1;
});

console.log(nextState);
// { number: 2, dontChangeMe: 2 }

콘솔에는...

리듀서에서 Immer 사용하기

우리가 알야할 점은 Immer 를 사용해서 간단해지는 업데이트도 있지만, 반대로 길어지는 업데이트도 존재한다.

예를들어 우리가 만들었던 프로젝트의 상태의 경우 users 배열이 객체의 깊은곳에 위치하지 않기 때문에 새 항목을 추가하거나 제거 할 때는 Immer 를 사용하는 것 보다 concat 이나 filter 를 사용하는것이 더 간편하고 편리하다.

사용법을 잘 알기위해 우리가 진행하고 있는 프로젝트에 사용해서 적용해보겠다.

App.js

import React, { useReducer, useMemo } from 'react';
import UserList from './UserList';
import CreateUser from './CreateUser';
import produce from 'immer';

function countActiveUsers(users) {
  console.log('활성 사용자 수를 세는중...');
  return users.filter(user => user.active).length;
}

const initialState = {
  users: [
    {
      id: 1,
      username: 'velopert',
      email: 'public.velopert@gmail.com',
      active: true
    },
    {
      id: 2,
      username: 'tester',
      email: 'tester@example.com',
      active: false
    },
    {
      id: 3,
      username: 'liz',
      email: 'liz@example.com',
      active: false
    }
  ]
};

function reducer(state, action) {
  switch (action.type) {
    case 'CREATE_USER':
      return produce(state, draft => {
        draft.users.push(action.user);
      });
    case 'TOGGLE_USER':
      return produce(state, draft => {
        const user = draft.users.find(user => user.id === action.id);
        user.active = !user.active;
      });
    case 'REMOVE_USER':
      return produce(state, draft => {
        const index = draft.users.findIndex(user => user.id === action.id);
        draft.users.splice(index, 1);
      });
    default:
      return state;
  }
}

// UserDispatch 라는 이름으로 내보내줍니다.
export const UserDispatch = React.createContext(null);

function App() {
  const [state, dispatch] = useReducer(reducer, initialState);

  const { users } = state;

  const count = useMemo(() => countActiveUsers(users), [users]);
  return (
    <UserDispatch.Provider value={dispatch}>
      <CreateUser />
      <UserList users={users} />
      <div>활성사용자 수 : {count}</div>
    </UserDispatch.Provider>
  );
}

export default App;

TOGGLE_USER 액션의 경우에는 Immer 를 사용해서 코드가 깔끔해졌지만, 나머지같은 경우에는 오히려 더 복잡해졌다.

우리는 상황에 따라 사용하면되고 모든 업데이트 로직에 사용할필요가 없다.

Immer 와 함수형 업데이트

우리는 이전에 useState 를 사용할 때, 함수형 업데이트란걸 배웠다.

예 )

const [todo, setTodo] = useState({
  text: 'Hello',
  done: false
});

const onClick = useCallback(() => {
  setTodo(todo => ({
    ...todo,
    done: !todo.done
  }));
}, []);

위처럼 setTodo 함수에 업데이트를 해주는 함수를 넣음으로써, 만약 useCallback 을 사용하는 경우 두번째 파라미터인 deps 배열에 todo 를 넣지 않아도 되게 한다.

이 때, Immer 를 사용하면 코드를 더 간단하게 작성할 수 있다.

만약에 produce 함수에다가 두개의 파라미터를 넣는다면, 첫 번째 파라미터에 넣은 상태를 불변성을 유지하면서 새로운 상태를 만들어주지만, 만약에 첫번째 파라미터를 생략하고 바로 업데이트 함수를 넣는다면, 반환 값은 새로운 상태가 아닌 상태를 업데이트 해주는 함수가 된다. 코드를 살펴보겠다.

const todo = {
  text: 'Hello',
  done: false
};

const updater = produce(draft => {
  draft.done = !draft.done;
});

const nextTodo = updater(todo);

console.log(nextTodo);
// { text: 'Hello', done: true }

결국에 produce 함수가 반환하는것이 업데이트 함수이기 때문에, useState 의 업데이트 함수를 사용할 때 우리는 밑에처럼 구현할 수 있다.

const [todo, setTodo] = useState({
  text: 'Hello',
  done: false
});

const onClick = useCallback(() => {
  setTodo(
    produce(draft => {
      draft.done = !draft.done;
    })
  );
}, []);

Immer 는 편한 라이브러리는 확실하다. 하지만, 성능면에서는 Immer 를 사용하지 않은 코드가 더 빠르다.

참고 : 벨로퍼트와 함께하는 모던 리액트

느낀점 :

  • 오늘은 Immer 이라는 라이브러리에 대해 알아보는 시간을가졌다.
  • React 에서는 불변성을 중요시여기는 것 같아 이 라이브러리도 잘 쓰여질 것 같다.
profile
하루를 의미있게 살자!

0개의 댓글