Immer를 사용하여 더 쉽게 불변성 유지하기

Yeummy·2021년 9월 7일
0

React

목록 보기
3/5
post-thumbnail

불변성이란?

불변성이란 React를 관통하는 핵심 키워드입니다.

불변성이란, 어떤 값을 직접적으로 변경하지 않고 새로운 값을 만들어 내는 것을 말합니다.

😒 원시 타입과 참조 타입

  1. 원시 타입

    원시타입은 Number, Boolean, null, undefined, String과 같이 5가지가 존재합니다.

  2. 참조 타입

    Object, Array, function이 있습니다.

먼저, 원시 타입 데이터는 변수에 할당될 때 메모리 상에 고정된 크기로 저장되고 해당 변수가 원시 데이터의 값을 보관합니다. 원시 타입 자료형은 변수 선언, 초기화, 할당시 값이 저장된 메모리 영역에 직접 접근합니다.

그러기 때문에, 변수에 새 값이 할당될 때마다, 변수에 할당된 메모리 블럭에 저장된 값을 바로 변경합니다. 이를 Access By Value라고 합니다.

참조 타입 데이터는 크기가 정해져있지 않고 변수에 할당될 때 값이 직접 해당 변수에 저장될 수 없고, 변수에는 데이터에 대한 참조만 저장됩니다. 즉, 변수에는 값이 저장된 힙 메모리의 주소값을 저장합니다.

정리하면, 참조타입은 변수의 값이 저장된 메모리 블럭의 주소를 가지고 있고 JS 엔진이 변수가 가지고 있는 메모리 주소를 이용해, 변수의 값에 접근합니다. → Access By Reference

🙄 그럼, 왜 불변성이라는 것을 지켜야 할까..?

불변성이란 어떤 값을 직접적으로 변경하지 않고 새로운 값을 만들어내는 것입니다.

위는 불변성에 대한 정의로, 우리는 불변성을 지키기 위해서 필요한 값을 변형해 사용하고 싶다면 어떤 값을 사본을 만들어 사용해야 합니다.

결론적으로, 원시 타입의 경우 직접 변경해도 상관 없지만 참조 타입의 경우 불변성에 대한 고려가 필요합니다.

const user = { name: 'Yeum', age: 25 };

const copyUser = user;

// user와 copyUser 변수에는 같은 참조값 즉, 같은 힙 영역의 메모리를 가지고 있습니다.

물론, 위와 같이 사용할 수 있지만 둘은 같은 힙 영역의 메모리를 가지고 있기 때문에 어느 하나가 값을 변경하면 어디에서 값을 바꾸었는지를 알 수 없습니다. → 에러 유발 가능성

그러기때문에 참조 타입의 경우에는 같은 메모리 주소를 공유하기 보다, 값 자체를 복사해서 전달해주는 것이 훨씬 좋습니다.

const user = { name: 'Yeum', age: 25 };

const copyUser = { ...user };

😎 그럼, 이제 스프레드 문법으로 복사만 하면 불변성은 다 지켜진것인가요?

안타깝게도, 아직 문제가 존재합니다.

우리가 위에서 사용했던 스프레드 연산자는 얕은 복사 ( Shallow Copy ) 즉, 한 단계까지만 복사하게 됩니다. 그러기 때문에 객체 안에 또 다른 참조 타입이 있다면 본래의 힙 메모리가 다시 또 복사되게 되는것이죠..

const user = { name: 'Choi', age: 25, friends: ['Park', 'Kim']}; 
const otherUser = { ...user }; 

user.name = 'Lee'; 
user.friends.push('Kang');

console.log(user === othorUser) // false
console.log(user.friends === othorUser.friends) // true

이와 같이 객체의 구조가 복잡해질수록 불변성의 유지가 어려워지게 됩니다.

그래서, Immer와 같은 라이브러리를 사용하게 되는 이유입니다.

Immer를 설치하고 사용법 알아보기

😆 프로젝트 준비

  1. yarn create react-app immer-tutorial
  2. cd immer-tutorial
  3. yarn add immer

☹️ immer를 사용하지 않고 불변성을 유지해보자..

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

export default function 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;
        },
        [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>
    );
}

이와 같이 전개 연산자와 배열 내장 함수를 사용해 불변성을 유지하면 객체가 깊어질 수 록 이를 유지하기가 매우 번거로워질 수 있습니다.

😼 Immer의 사용법을 알아보자.

immer를 사용하면 불변성을 유지하는 작업을 매우 간단하게 처리할 수 있습니다.

produce 함수의 정의는 다음과 같습니다.

produce(currentState, recipe: (draftState) => void): nextState

produce 함수는 두 가지 파라미터를 받습니다.

  1. 수정하고 싶은 상태 → 현재의 상태
  2. 상태를 어떻게 업데이트 할 것인지 → 변경할 상태

두 번째 콜백 함수로 전달되는 함수 내부에서 원하는 값을 변경하면 produce 함수가 불변성 유지를 대신해 주면서 새로운 상태를 생성해 줍니다.

import produce from "immer"

const baseState = [
    {
        title: "Learn TypeScript",
        done: true
    },
    {
        title: "Try Immer",
        done: false
    }
]

const nextState = produce(baseState, draftState => {
    draftState.push({title: "Tweet about it"})
    draftState[1].done = true
})

⇒ 두 번째 콜백 함수에서 내가 원하는 값을 변경하면 편리하게 불변성을 유지할 수 있다.

두 번째 콜백함수에는 일반적으로 draft 를 전달해주는데, 이 단어는 "초안" 이라는 뜻으로 복사된 객체를 우리가 수정해 최종본으로 만들어 반환하는 방법이라고 생각하면 될 것 같습니다.

🙈 기존 코드에 Immer를 적용해보자.

//input 수정을 위한 함수
    const onChange = useCallback(
        e => {
            const { name, value } = e.target;
            setForm(
                produce(form, draft => {
                    draft[name] = value;
                })
                //   {
                //     ...form,
                //     [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(info);
                })
                // {
                //   ...data,
                //   array: data.array.concat(info),
                // }
            );

            // form 초기화
						...
        [form.name, form.username]
    );
const onRemove = useCallback(
        id => {
            setData(
                produce(data, draft => {
                    draft.array.splice(
                        draft.array.findIndex(info => info.id === id),
                        1
                    );
                })
                //   {
                //     ...data,
                //     array: data.array.filter(info => info.id !== id),
                // }
            );
        },
        [data]
    );

이와 같이, 매우 직관적으로 불변성을 유지할 수 있습니다.

😊 useState의 함수형 업데이트와 immer 함께 사용하기

immer에서 제공하는 produce 함수를 호출시, 첫 번째 파라미터에 콜백 함수로 선언하고 원본 객체를 전달하면 업데이트를 한 객체 복사본이 전달됩니다.

const update = produce(draft => {
	draft.value = 2;
});

const originalState = {
	value: 1,
	foo: 'bar'
};

const updateState = update(originalState);
console.log(updateState); // { value: 2, foo: 'bar' }

🤩 그럼, 이 또한 Immer를 사용해서 바꿔봅시다.

//input 수정을 위한 함수
const onChange = useCallback(
    e => {
        const { name, value } = e.target;
        setForm(
            produce(draft => {
                draft[name] = value;
            })
            // 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(draft => {
                    draft.array(info);
                })
                // produce(data, draft => {
                //     draft.array(info);
                // })
            );

						...

            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
                    );
                })
                // produce(data, draft => {
                //     draft.array.splice(
                //         draft.array.findIndex(info => info.id === id),
                //         1
                //     );
                // })
            );
        },
        [data]
    );

참조

profile
The BaekSu Redemption

0개의 댓글