상세 컨텐츠

본문 제목

<리액트를 다루는 기술> 12장: immer를 사용하여 더 쉽게 불변성 유지하기

21-22/21-22 리액트 스타터 -2

by dev otcroz 2022. 1. 17. 13:01

본문

728x90

INDEX

12장 immer를 사용하여 더 쉽게 불변성 유지하기

1. immer를 설치하고 사용법 알아보기

1.1 프로젝트 준비

1.2 immer를 사용하지 않고 불변성 유지

1.3 immer 사용법

1.4 App 컴포넌트에 immer 적용하기

1.5 useState의 함수형 업데이트와 immer 함께 쓰기

2. Question 개념 정리 및 코드 문제

● 개념 복습 문제

● 코드 문제


immer 라이브러리

구조가 복잡한 객체도 매우 쉽고 짧은 코드를 사용해 불변성을 유지하며 업데이트할 수 있게 해주는 라이브러리

 

불변성을 유지하며 업데이트해야할 때 immer를 사용하지 않으면?

  • 배열 또는 객체를 복사할 때: 전개 연산자(...)배열의 내장 함수 사용 
  • 객체의 구조가 엄청나게 깊어지면: 하나의 값을 업데이트하는 데에도 전개 연산자를 여러 번 사용한 매우 긴 코드가 필요함

1. immer를 설치하고 사용법 알아보기

1.1  프로젝트 준비

터미널에서 다음 명령을 수행한다.

$ yarn create react-app immer-tutorial

$ cd immer-tutorial

$ yarn add immer


1.2  immer를 사용하지 않고 불변성 유지

immer를 사용하지 않고 불변성을 유지하면서 값을 업데이트하는 컴포넌트를 작성해보자.

// App.js

import { useRef, useCallback, useState } from 'react';

const App = () => {
  const nextId = useRef(1);
  const [form, setForm] = useState({ name: '', username: '' });
  const [data, setData] = useState({
    array: [],
    uselessValue: null
  });

  // input 수정을 위한 함수
  const onChange = useCallback(e => {
    const { name, value } = e.target;
    setForm(
      {
        ...form,
        [name]: [value]
      }
    );
  }, [form]);

  // form 등록을 위한 함수
  const onSubmit = useCallback(
    e => {
      e.preventDefault();
      const info = {
        id: nextId.current,
        name: form.name,
        username: form.username
      };

      // array 에 새 항목 등록
      setData({
        ...data,
        array: data.array.concat(info)
      });

      // form 초기화
      setForm({
        name: '',
        username: ''
      });
      nextId.current += 1;
    },
    [data, form.name, form.username]
  );

  // 항목을 삭제하는 함수
  const onRemove = useCallback(
    id => {
      setData({
        ...data,
        array: data.array.filter(info => info.id !== id)
      });
    },
    [data]
  );

  return (
    <div>
      <form onSubmit={onSubmit}>
        <input
          name="username"
          placeholder="아이디"
          value={form.username}
          onChange={onChange}
        />
        <input
          name="name"
          placeholder="이름"
          value={form.name}
          onChange={onChange}
        />
        <button type="submit">등록</button>
      </form>
      <div>
        <ul>
          {data.array.map(info => (
            <li key={info.id} onClick={() => onRemove(info.id)}>
              {info.username} ({info.name})
            </li>
          ))}
        </ul>
      </div>
    </div>
  );
};

export default App;

이 컴포넌트의 기능

폼에서 아이디/이름 입력 시 ▶ 하단 리스트에 추가

리스트 항목 클릭 시 ▶ 삭제


1.3  immer 사용법

// 예시 코드: 구조가 깊은 객체에서 값을 변경할 때

import produce from 'immer';
const nextState = produce(originalState, draft => {
  // 바꾸고 싶은 값 바꾸기
  draft.somewhere.deep.inside = 5;
})

produce(상태, 함수)

  • 1번째 파라미터: 수정하고 싶은 상태
  • 2번째 파라미터: 상태를 어떻게 업데이트할지 정의하는 함수

'불변성에 신경 쓰지 않는 것처럼 코드를 작성하되, 불변성 관리는 제대로 해주는 것' 

produce 함수불변성 유지 작업을 대신해주면서 새로운 상태를 생성해주므로,

프로그래머는 쉽고 짧은 코드로도 불변성을 유지하며 원하는 값만 업데이트할 수 있다.

 

깊은 곳에 위치하는 값을 바꾸는 경우 외에, 배열을 처리할 때에도 매우 쉽고 편하다.

// 예시 코드: 배열, 객체를 모두 관리하는 복잡한 데이터

import produce from 'immer';

const originalState = [
  {
    id: 1,
    todo: '전개 연산자와 배열 내장 함수로 불변성 유지하기',
    checked: true,
  },
  {
    id: 2,
    todo: 'immer로 불변성 유지하기',
    checked: false,
  }
];

const nextState = produce(originalState, draft => {
  // id가 2인 항목의 checked 값을 true로 설정
  const todo = draft.find(t => t.id === 2);  // id로 항목 찾기
  todo.checked = true;  // 혹은 draft[1].checked = true;
  
  // 배열에 새로운 데이터 추가
  draft.push({
  id: 3,
  todo: '일정 관리 앱에 immer 적용하기',
  checked: false,
  });
  
  // id = 1인 항목을 제거하기
  draft.splice(draft.findIndex(t => t.id === 1), 1);
});

 


1.4  App 컴포넌트에 immer 적용하기

1.2에서 만든 App 컴포넌트에 immer를 적용해 더욱 깔끔한 코드로 만들어보자.

import { useRef, useCallback, useState } from 'react';
import produce from 'immer';

const App = () => {
  const nextId = useRef(1);
  const [form, setForm] = useState({ name: '', username: '' });
  const [data, setData] = useState({
    array: [],
    uselessValue: null
  });

  // input 수정을 위한 함수
  const onChange = useCallback(e => {
    const { name, value } = e.target;
    setForm(
      produce(form, draft => {
        draft[name] = value;
      })
    );
  }, [form]);

  // form 등록을 위한 함수
  const onSubmit = useCallback(
    e => {
      e.preventDefault();
      const info = {
        id: nextId.current,
        name: form.name,
        username: form.username
      };

      // array 에 새 항목 등록
      setData(
        produce(data, draft => {
          draft.array.push(info);
        })
      );

      // form 초기화
      setForm({
        name: '',
        username: ''
      });
      nextId.current += 1;
    },
    [data, form.name, form.username]
  );

  // 항목을 삭제하는 함수
  const onRemove = useCallback(
    id => {
      setData(
        produce(data, draft => {
          draft.array.splice(draft.array.findIndex(info => info.id === id), 1);
        })
      );
    },
    [data]
  );

  return (
    <div>
      <form onSubmit={onSubmit}>
        <input
          name="username"
          placeholder="아이디"
          value={form.username}
          onChange={onChange}
        />
        <input
          name="name"
          placeholder="이름"
          value={form.name}
          onChange={onChange}
        />
        <button type="submit">등록</button>
      </form>
      <div>
        <ul>
          {data.array.map(info => (
            <li key={info.id} onClick={() => onRemove(info.id)}>
              {info.username} ({info.name})
            </li>
          ))}
        </ul>
      </div>
    </div>
  );
};

export default App;

immer를 사용하여 컴포넌트 상태를 작성할 때

  • 객체 안의 값을 직접 수정 가능
  • 배열에 직접적인 변화를 일으키는 push, splice 등의 배열 내장 함수 사용 가능

 

immer를 사용한다고 해서 무조건 코드가 간결해지지는 않는다. 복잡한 코드를 정리할 때에만 사용해도 충분하다.

예) 위 예시의 onRemove의 경우 배열 내장 함수 filter를 사용하는 코드가 더 깔끔하므로, immer를 적용할 필요가 없다.


1.5  useState의 함수형 업데이트와 immer 함께 쓰기

 

11장에서 공부한 useState의 함수형 업데이트

const [number, setNumber] = useState(0);
// prevNumbers는 현재 number 값을 가리킨다.
const onIncrease = useCallback(
  () => setNumber(prevNumber => prevNumber +1),
  [],
);

immer에서 제공하는 produce 함수를 호출할 때, 첫 번째 파라미터가 함수 형태라면 produce()는 업데이트 함수를 반환한다.

const update = produce(draft => {
  draft.value = 2;
});
const originalState = {
  value: 1,
  foo: 'bar',
};
const nextState = update(originalState);
console.log(nextState); // { value: 2, foo: 'bar' }

이렇게 반환된 업데이트 함수와 useState의 함수형 업데이트를 함께 활용하면 코드를 더욱 깔끔하게 만들 수 있다.

// App.js

import { useRef, useCallback, useState } from 'react';
import produce from 'immer';

const App = () => {
  const nextId = useRef(1);
  const [form, setForm] = useState({ name: '', username: '' });
  const [data, setData] = useState({
    array: [],
    uselessValue: null
  });

  // input 수정을 위한 함수
  const onChange = useCallback(e => {
    const { name, value } = e.target;
    setForm(
      produce(draft => {
        draft[name] = value;
      })
    );
  }, []);

  // form 등록을 위한 함수
  const onSubmit = useCallback(
    e => {
      e.preventDefault();
      const info = {
        id: nextId.current,
        name: form.name,
        username: form.username
      };

      // array 에 새 항목 등록
      setData(
        produce(draft => {
          draft.array.push(info);
        })
      );

      // form 초기화
      setForm({
        name: '',
        username: ''
      });
      nextId.current += 1;
    },
    [form.name, form.username]
  );

  // 항목을 삭제하는 함수
  const onRemove = useCallback(
    id => {
      setData(
        produce(draft => {
          draft.array.splice(draft.array.findIndex(info => info.id === id), 1);
        })
      );
    },
    []
  );

  return (
    <div>
      <form onSubmit={onSubmit}>
        <input
          name="username"
          placeholder="아이디"
          value={form.username}
          onChange={onChange}
        />
        <input
          name="name"
          placeholder="이름"
          value={form.name}
          onChange={onChange}
        />
        <button type="submit">등록</button>
      </form>
      <div>
        <ul>
          {data.array.map(info => (
            <li key={info.id} onClick={() => onRemove(info.id)}>
              {info.username} ({info.name})
            </li>
          ))}
        </ul>
      </div>
    </div>
  );
};

export default App;

2. Question 개념 정리 및 코드 문제

● 개념 복습 문제

1.  immer 라이브러리를 사용하면 간단한 코드만으로 (불변성을 유지)하며 원하는 값을 업데이트할 수 있다.

2. 기존의 방법으로는 객체, 배열로 이루어진 복잡한 데이터를 다룰 때 (전개 연산자(...))와 (배열 내장 함수)를 여러 번 사용해야 했다.

3. immer 라이브러리의 (produce) 함수는 첫 번째 파라미터로 (수정하고 싶은 상태), 두 번째 파라미터로 (상태를 어떻게 업데이트할지 정의하는 함수)를 받는다.

4. immer를 사용하여 컴포넌트 상태를 작성할 때는 객체 안에 있는 값을 (직접 수정하거나), 배열에 직접적인 변화를 일으키는 push, splice 등의 (배열 내장 함수)를 사용해도 무방하다.

5. immer를 사용했을 때 항상 코드가 (간결해지는 것은) 아니므로 불변성을 유지하는 코드가 (복잡할) 때에만 사용하면 된다.

6. produce 함수의 첫 번째 파라미터가 함수 형태라면 (업데이트 함수)를 반환한다.

7. 6. 같은 특성과 (useState)의 (함수형 업데이트)를 함께 활용하면 코드를 더욱 깔끔하게 할 수 있다.

 

● 코드 문제

1. 다음 컴포넌트에 immer 라이브러리를 적용해 간결하게 만든다.

2. useState의 함수형 업데이트를 함께 사용해 코드를 더욱 간단하게 만든다.

import { useCallback, useState } from 'react';

const App = () => {
  const [student, setStudent] = useState({ name: '', ID: '' });
  const [data, setData] = useState({
    array: [],
    uselessValue: null
  });

  // input 수정을 위한 함수
  const onChange = useCallback(e => {
    const { name, value } = e.target;
    setStudent(
      {
        ...student,
        [name]: [value]
      }
    );
  }, [student]);

  // form 등록을 위한 함수
  const onSubmit = useCallback(
    e => {
      e.preventDefault();
      const info = {
        id: student.ID,
        name: student.name,
      };

      // array 에 새 항목 등록
      setData({
        ...data,
        array: data.array.concat(info)
      });

      // form 초기화
      setStudent({
        name: '',
        ID: ''
      });
    },
    [data, student.name, student.ID]
  );

  // 항목을 삭제하는 함수
  const onRemove = useCallback(
    id => {
      setData({
        ...data,
        array: data.array.filter(info => info.id !== id)
      });
    },
    [data]
  );

  return (
    <div>
      <form onSubmit={onSubmit}>
        <input
          name="ID"
          placeholder="학번"
          value={student.ID}
          onChange={onChange}
        />
        <input
          name="name"
          placeholder="이름"
          value={student.name}
          onChange={onChange}
        />
        <button type="submit">등록</button>
      </form>
      <div>
        <ul>
          {data.array.map(info => (
            <li key={info.id} onClick={() => onRemove(info.id)}>
              {info.id} / {info.name}
            </li>
          ))}
        </ul>
      </div>
    </div>
  );
};

export default App;

Corner React Starter #2

Editor 유즈

728x90

관련글 더보기