본문으로 바로가기

12. immer

category React/리액트를 다루는 기술 2021. 2. 5. 01:01

12-1. 복잡한 불변성 유지


const obj1 = {
    arr : [1,2,3,4]
}
const obj2 = {...obj1};

이렇게 값을 복사한 코드는 과연 불변성을 유지하고 있을까?

	console.log(obj1===obj2); // false
  	obj1.arr.pop(); 
  	console.log(obj2); // [1,2,3]

'정답은 그렇지 않다' 이다

얼핏보면 불변성을 유지하고 있는 것 같지만 객체 안의 객체인 배열은 서로 같은 대상을 참조하고 있는 상태

그렇기 때문에 내부에 객체가 있는 state의 불변성을 유지한 상태로 값을 변경시키려면 어떻게 해야할까?

obj 내부의 inner.value값을 불변성을 유지하며 2로 업데이트해보자

const obj = {
        id: 1,
        inner :{
                test : 2,
                test2 : 3,
                value : 1,
        }
}
const nextObj = {
        ...obj,
        inner: {
                ...obj.inner,
                value : 2
        }
}

객체의 깊이가 깊어질 수록 코드의 가독성이 매우 매우 나빠진다

12-2. immer


이렇게 나빠지는 가독성과 불편함을 지켜보고 있을 수만은 없기 때문에 immer라는 라이브러리를 사용하면 편리하게 불변성을 유지하며 state를 업데이트할 수 있다

import './App.css';
import React, {useCallback, useRef, useState} from "react";

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

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

    const handleSubmit = useCallback((e) => {
        e.preventDefault();
        const temp = {
            ...form,
            id: nextId.current,
        };
        setData({
            ...data,
            array : data.array.concat(temp),
        });

        nextId.current++;
        setForm({name:'',username:''});
    }, [form, data]);

    const handleRemove = (id) =>{
        setData({
            ...data,
            array : data.array.filter(v=>v.id!==id),
        })
    }

    return (
        <div>
            <form onSubmit={handleSubmit} >
                <input name={"username"} placeholder={'ID'} value={form.username} onChange={handleChange}/>
                <input name={'name'} placeholder={'name'} value={form.name} onChange={handleChange}/>
                <button type={"submit"} >등록</button>
            </form>
            <ul>
                {data.array.map((v)=><li key={v.id} onClick={()=>handleRemove(v.id)}> name : {v.name}, username : ({v.username})</li>)}
            </ul>
        </div>
    )
}
export default App;

이때까지 해온 예제와 비슷한데 username,과 name을 받아서 data의 array에 넣고 출력하는 매우 간단한 컴포넌트다

다른점이 있다면 data에 uselessValue하나를 넣어주었다는 것인데 쓸모없는 속성값이 들어간 state의 업데이트가 얼마나 귀찮아졌는지를 확인할 수 있다

이제 immer를 써보자

12-2-1. 사용법


immer는 다음과 같이 사용할 수 있다


const nextState = producer(originalState, draft=>{
        // 12-1의 3번 예제와 동일하게 작동한다
        draft.inner.value = 2;
});

producer함수는 두 가지 파라미터를 받는다

originalState는 내가 수정하고 싶은 state이고, 두 번째로 받는 함수는 상태 업데이트를 지시하는 콜백 함수다

const obj = [
        {id : 1, text : 'something', checked: true},
        {id : 2, text : 'nothing', checked: false},
];

const nextObj = producer(obj, draft=>{
        // 인덱스가 2인 원소를 찾아 체크하기
        const todo = draft.find(v=> v.id===2);
        todo.checked = true;

        // 원소 추가하기
        draft.push({id : 3, text : '...', checked: false});

        // 인덱스 1인 원소를 제거하기
        draft.splice(draft.findIndex(v=>v.id===1),1);
});
setObj(nexObj);

보면 알 수 있듯이 우리가 불변성을 신경써주며 객체를 복사하거나 건드리는 것이 아니라

producer함수가 state를 draft에 불변성을 유지해서 복사시켜주고 우리는 draft를 편하게 직접적으로 수정해주는 함수를 넘겨주면 된다

그러면 우리가 지시한대로 지지고 볶은 draft를 반환해준다

12-2-2. 적용하기


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

    const handleSubmit = useCallback((e) => {
        e.preventDefault();

        setData(produce(data, draft=>{
            draft.array.push({...form,id:nextId.current});
        }));

        nextId.current++;

        setForm({name:'',username:''});
    }, [form]);

    const handleRemove = (id) =>{
        setData(produce(data, draft=>{
            draft.array.splice(draft.array.findIndex(v=>v.id===id),1);
        }));
    }

handleChange는 미리 선언한 변수에 producer의 반환값을 받아서 업데이트해주었고 그 밑으로는 바로 producer 반환값을 set...들에게 건네주어서 상태값을 없데이트해주었다

12-2-3. 함수형 업데이트


바로 이전장에서 useState의 함수형 업데이트를 배웠다

state의 값을 파라미터로 받고 함수에서 업데이트를 지시해주고 반환하는 것이었는데 producer함수에도 똑같이 함수형 업데이트 적용이 가능하다

첫 번째 파라미터로 업데이트를 지시할 콜백 함수를 넣어주면된다

        const handleChange = (e) => {
        setForm(produce(draft=>{
            draft[e.target.name] = e.target.value;
        }));
    };

    const handleSubmit = useCallback((e) => {
        e.preventDefault();

        setData(produce(draft=>{
            draft.array.push({...form,id:nextId.current});
        }));

        nextId.current++;

        setForm({name:'',username:''});
    }, [form, data]);

    const handleRemove = (id) =>{
        setData(produce(draft=>{
            draft.array.splice(draft.array.findIndex(v=>v.id===id),1);
        }));
    }

코드가 훨씬 간결해졌다