본문 바로가기

나야, 리액트 스터디

[week4] ... state 왜 파도파도 계속 나와요? (batch, immer 등 리액트 state 변경에 대해 알아보자)

 

안녕하세요! 물결웹팟 OB 박채연입니다.

요즘 제가 취미가 하나 생겼는데요? 바로 스터디원들에게 커피 돌리기 입니다 ~!

 

장난이구요.

아티클 지각해서 죄송합니다 ㅎ

변명을 하나 해보자면, 합세에.. 스터디에.. 이것저것 약속에 치이다보니 일요일이 후루룩 지나가더라구요?

물론 여러분도 레전드 바쁜 와중에 시간내서 공부한걸테니 좋은 변명이 아니라고 생각합니다 하하

어쨌든 벌칙 수행을 위해 나리스 여러분께 커피 돌릴 예정이니 댓글에 원하는 음료 남겨주세요 ^^

 

그럼 주절주절 그만하고 호다닥 공부한 내용 공유해보겠습니다 ! 

 


 

 

 

Batch

리액트에선 setState() 혹은 hook을 사용해서 컴포넌트의 state를 변경합니다.

그리고 state가 변경되면 리렌더링이 발생하며, 페이지의 변화를 나타냅니다.

 

그렇지만, 우리가 만드는 다양한 어플리케이션은 정말 많은 state로 구성되어 있고,

여러 상호작용을 거치며 무수히 많은 state의 변화가 나타나게 되면서, 많은 리렌더링이 발생할텐데요,

리렌더링이 많이 발생할수록 프로그램은 무거워지고 여러 부담이 증가하게 됩니다.

리액트에선 이런 리랜더링의 비용을 줄이기 위해서 batch 라는 과정을 거치며 state를 관리합니다.

 

- 예시

      <button onClick={() => {
        setNumber(number + 1);
        setNumber(number + 1);
        setNumber(number + 1);
      }}>+3</button>

 

위 코드를 보면, set 함수가 세 번 호출되었고, 모두 number 값에 +1을 하는 동작을 하고자 합니다.

그치만, 각 렌더링의 state 값은 스냅샷을 찍은 것처럼 고정되어 있기 때문에, 

number는 setNumber(number + 1)을 몇 번 호출하더라도 항상 0으로 남게 됩니다.

 

또한, 리액트는 state를 업데이트 하기 전, 이벤트 핸들러의 모든 코드가 실행될 때까지 기다리는데요.

따라서, 리렌더링은 모든 setNumber() 호출이 완료된 이후 일어나게 됩니다.

 

이런 batching 동작으로 인해, 너무 많은 리렌더링이 발생하지 않고

여러 컴포넌트에서 나온 다수의 state 변수를 업데이트 할 수 있습니다.

(= 이벤트 핸들러와 그 안에 있는 코드가 완료되기 전까지 화면 UI에 반영이 되지 않습니다.)

 

 

- 업데이터 함수

또한, 이 경우 우리는 늘 prev => prev + 1 형태로 set 함수에 적용해서 값을 업데이트 하고자 하는데,

이때 n => n + 1 형태를 업데이터 함수 updater function 이라 칭하며, 이를 state 설정자 함수에 전달할 경우

리액트는 이벤트 핸들러의 다른 모든 코드가 실행된 후, 해당 함수가 처리되도록 큐에 넣고

다음 렌더링 중 큐를 순회하며 최종 업데이트 된 state를 제공하는 원리입니다.

 

setNumber(n => n + 1);
setNumber(n => n + 1);
setNumber(n => n + 1);

 

이 코드를 기준으로 보자면, 

 

setNumber(n => n + 1): n => n + 1 함수를 큐에 추가
setNumber(n => n + 1): n => n + 1 함수를 큐에 추가
setNumber(n => n + 1): n => n + 1 함수를 큐에 추가

 

이렇게 각각의 set 함수를 큐에 추가합니다.

또, 다음 렌더링 중 useState를 호출하면, 리액트는 이 큐를 순회하며 return 하는 거죠!

 

첫번째 n => n + 1 일 때 n은 0이기 때문에 1를 return
두번째 n => n + 1 일 때 n은 1이기 때문에 2를 return
세번째 n => n + 1 일 때 n은 2이기 때문에 3을 return 

 

그리고 3을 최종 결과로 저장해서 useState에서 반환하게 됩니다.

여기서 useState의 반영이 한 텀 늦게 되는 이유도 같이 챙겨갈 수 있겠죠?

 

그리고 하나만 더 챙겨가자면, setState(5)는 실제로 setState(n => 5) 형태이기 때문에 

n은 사용되지 않고, 그대로 5가 return 된다고 합니다!

 

 

- 명명 규칙

업데이터 함수의 인수 이름은 state 변수의 첫 글자로 지정하거나,

변수의 이름을 반복하는 형태로 / 혹은 prev 같은 접두사를 붙이는 것이 규칙으로 많이 쓰인다고 합니다.

(늘 prev 만 반복해서 작성하던 것보다, 변수명을 이용해서 좀 더 세심하게 state를 다뤄봐야겠어요)

 

 


 

 

Immer

불변성을 유지하면서도, 직접 수정하는 것처럼 코드를 작성할 수 있게 도와주는 라이브러리라고 합니다.

 

리액트에서 상태를 업데이트 할 때, 불변성을 지키는 것은 매우 중요합니다.

따라서 우리는, 배열이나 객체 상태를 업데이트 할 때, 참조하고 있는 값을 변경하지 않고 새로운 배열이나 객체에 기존값을 복사하고, 그 안에서 값을 조작한 후 이를 통해 반영하는 형식으로 배열과 객체를 다루곤 하죠?

 

이번 공식문서를 읽으며 이 라이브러리를 통해 이런 과정을 더 간편하게 구현할 수 있다는 걸 처음 알게 되어서,

이 부분에 대해서도 함께 공부해봤습니다.

 

방금 언급한 것처럼, 일반적으로 새로운 값을 교체할 때, 우린 전개 연산자를 사용해 복사하곤 하는데,

이런 전개연산자는 객체나 배열 내부 값을 복사할 때 가장 바깥쪽 값만 복사하게 됩니다.

즉, 객체 안에 이중, 삼중으로 객체가 또 들어있다면, 이 내부 값들을 또 각각 전개연산자로 복사할 필요가 있다는 거죠!

이 과정을 더 간편하게 해주는 친구가 바로 immer 라고 합니다.

 

- 사용 방법

 

(1) 라이브러리 설치

yarn add immer

 

(2) produce 함수 사용

첫번째 파라미터엔 수정하고 싶은 상태를,  

두번째 파라미터엔 상태를 어떻게 업데이트 할 지 정의하는 함수를 넣으면 됩니다.

(만약, produce를 첫번째 파라미터가 함수 형태라면, 업데이트 함수를 반환하게 됩니다.)

 

import produce from 'immer';

const originState = [
    {
        id : 1,
        txt : 'hello'
    },
    {
        id : 2,
        txt : 'bye'
    }
];

const nextState = produce(originState, draft => {
    // id로 항목 찾아서 수정하기
    const text = draft.find(t => t.id == 2);
    text.txt = 'hello world!!';

    // 새로운 데이터 추가
    draft.push({
        id : 3,
        txt : 'good night'
    })

    // id로 항목 찾아서 제거
    draft.splice(draft.findIndex(t = t.id == 1));
})

 

이런 immer를 사용하면 컴포넌트 상태 작성 시,

객체 안의 값을 직접 수정하거나 직접적으로 변화를 일으키는 push, splice를 사용해도 괜찮다고 합니다.

 

 

- 상태 업데이트 원리

(1) immer는 원본 객체를 proxy로 감싸서, 상태를 감지할 수 있는 dreft 객체를 생성하고,

(2) draft 객체에서 발생하는 모든 변경을 추적하는데, 만약 draft 객체에서 변경이 발생하면 immer는 이 변경사항을 기록하고 이를 기반으로 shallow copy를 생성한다고 합니다.

(3) 변경되지 않은 부분은 기존 객체의 참조를 재사용하고, 변경된 부분만 새로운 객체로 생성합니다.

 

=> 메모리 사용을 최적화하고, 불필요한 복사를 방지하기 때문에 효율적인 방법이라 할 수 있죠!

 

 


 

궁금증

마지막으로 저의 궁금증 하나 풀어보겠습니다! 저는 이번에 immer에 대해 알아보면서 효율적인 방식으로 객체나 배열을 업데이트 할 수 있다는 점에 있어서 너무 좋은 방식이라는 생각이 들었지만, 스프레드 연산자를 이용해서 객체나 배열을 복사하며 코드를 작성하는게 더 익숙해서 그런지, immer 가 그렇게 편하고 간결한가? 라는 생각도 들더라구요. 또, 라이브러리를 사용하는 것도 좋지만, 이런 객체나 배열 복사하는게 그렇게 어려운 작업이 아니라고 생각하는데, 이것까지 라이브러리를 사용해서 개발하는 게 맞나?라는 생각이 들기도 했어요. (공식문서에 언급된 라이브러리라 뭐 좋은 건 알겠다만!) 

 

나리스 스터디원 분들은 이 부분에 대해서 어떻게 생각하시는지 궁금합니다.

라이브러리를 보통 어떨 때 도입하고, 어떤 기준을 가지고 계신가요!

꼭 immer가 아니더라도, 캐러셀 구현할 때나, 캘린더 구현할 때나, 더 크게 가면 api 연동할 때나

라이브러리 도입에 대해 고민할 때, 어떤 기준으로 결정을 내리시는지 궁금해요! 

구체적인 경험들도 함께 들려주신다면 좋을 것 같습니다!