티스토리 뷰

WEB/React

[React] 불변성 유지: immer

춘햄 2023. 3. 29. 17:17

직전 포스팅에서 내용을 좀 다루기는 했는데, 전개 연산자와 배열의 내장 함수를 사용하면 간단하게 배열 혹은 객체를 복사하고 새로운 값을 덮어 쓸 수 있다는 걸 알았다. 

 

하지만 객체의 구조가 엄청나게 깊어지면 불변성을 유지하면서 이를 업데이트하는 것이 매우 힘들다.

 

아래 예제 코드를 보면 확실하게 이해할 수 있다.

const object = {
    somewhere: {
        deep: {
            inside: 3,
            array: [1, 2, 3, 4]
        },
        bar: 2
    },
    foo: 1
};

// somewhere.deep.inside 값을 4로 바꾸기
let nextObject = {
    ...object,
    somewhere: {
        ...object.somewhere,
        deep: {
            ...object.somewhere.deep,
            inside: 4
        }
    }
};

무슨 inside 값 하나를 업데이트하기 위해 코드를 열 줄은 작성해야 한다.

 

간혹 실제 프로젝트에서도 이렇게 복잡한 상태를 다룰 때가 있는데, 그럴 때마다 전개 연산자를 여러 번 사용하는 것은 꽤나 번거로운 일이다.

 

이러한 상황에 immer이라는 라이브러리를 사용하면, 구조가 복잡한 객체도 매우 쉽고 짧은 코드를 사용하여 불변성을 유지하면서 업데이트해 줄 수 있다.

 

npm install immer로 immer를 설치해보고 예제를 작성해보자.


우선, App 컴포넌트를 아래와 같이 작성하여 input 에 이름과 아이디를 입력하면 리스트에 띄우도록 구현한다.]

 

◎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
    });

    const onChange = useCallback(
        e => {
            const {name, value} = e.target;
            setForm({
                ...form,
                [name]: [value]
            });
        }, [form]
    );

    const onSubmit = useCallback(
        e => {
            e.preventDefault();
            const info = {
                id: nextId.current,
                name: form.name,
                username: form.username
            };

            setData({
                ...data,
                array: data.array.concat(info)
            });

            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;

코드를 보면, data나 form 과 같은 state를 전개 연산자를 사용하여 복사하는 방식으로 불변성을 유지하고 있다.


immer 사용법

immer를 사용하면 불변성을 유지하는 작업을 매우 간단하게 처리할 수 있다. 이 라이브러리의 사용법은 아래와 같다.

 

import produce from 'immer';

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

produce라는 함수는 두 가지 파라미터를 받는다. 첫 번째 파라미터는 수정하고 싶은 상태이고, 두 번째 파라미터는 상태를 어떻게 업데이트할 지 정의하는 함수이다. 

 

두 번째 파라미터로 전달되는 함수 내부에서 원하는 값을 변경하면, produce 함수가 불변성 유지를 대신해 주면서 새로운 상태를 생성해준다.

 

이 라이브러리의 핵심은 '불변성에 신경 쓰지 않는 것처럼 코드를 작성하되 불변성 관리는 제대로 해 주는 것' 이다. 

 

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

 

바로 App 컴포넌트에 적용해보자.

 

◎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
    });

    const onChange = useCallback(
        e => {
            const {name, value} = e.target;
            setForm(
               produce(form, draft => {
                   draft[name] = value;
               })
            );
        }, [form]
    );

    const onSubmit = useCallback(
        e => {
            e.preventDefault();
            const info = {
                id: nextId.current,
                name: form.name,
                username: form.username
            };

            setData(
                produce(data, draft => {
                    draft.array.push(info);
                })
            );

            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 (
    	...
    );
}


export default App;

 전개 연산자를 사용하여 state를 복사하고, 업데이트하는 과정을 알아서 처리해주니 코드를 좀 더 간결하게 바꿀 수 있다.

 

물론, immer에서 제공하는 produce 함수를 호출할 때, 첫 번째 파라미터가 함수 형태라면 업데이트 함수를 반환하는 useState의 함수형 업데이트를 함께 활용하면 더 깔끔하게 사용할 수 있다.

 

◎App.js


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
        };

        setData(
            produce(draft => {
                draft.array.push(info);
            })
        );

        setForm({
            name: '',
            username: ''
        });

        nextId.current += 1;
    }, [data, form.name, form.username]
);

const onRemove = useCallback(
    id => {
        setData(
            produce(draft => {
                draft.array.splice(draft.array.findIndex(info => info.id === id), 1);
            })
        );
    }, [data]
);

끝!

반응형

'WEB > React' 카테고리의 다른 글

[React] 외부 API 활용: News viewer  (0) 2023.03.30
[React] Routing: React Router  (0) 2023.03.30
[React] Todo List: 컴포넌트 성능 최적화  (0) 2023.03.29
[React] First Project: Todo List  (0) 2023.03.28
[React] 컴포넌트 스타일링  (0) 2023.03.27
Comments