[React] 참조 타입과 setState - 배열/객체 상태관리 완벽 정리

jinyoung·2022년 11월 22일
3
post-thumbnail

배열이나 객체로 setState하기

react에서 arrayobject 형태의 state을 다룰때 유의해야할 부분이 있다. 그것이 어떤 것이며 왜 그런지 알아보자.

목표: 배열이나 객체 데이터 등 참조 자료형 데이터의 변동을 리액트가 원활하게 감지하여 정상 작동하도록 한다.


JavaScript 데이터 타입 종류

자바스크립트에서 자료형은 크게 원시 자료형(Primitive Type)참조 자료형(Reference Type)으로 나뉜다.

원시 자료형은 number, string, boolean 등과 같이 변수 당 하나의 데이터만 저장되는 특징이 있으며, 새 값 할당 시 참조 주소값의 변동이 아닌, 메모리에 저장된 값이 바로 바뀐다.

반면 참조 자료형은 object, array, set과 같이 원시 자료형이 아닌 것을 말한다. 변수 선언 시 메모리의 주소값이 저장되며, 다른 변수에 할당 할 때 해당 주소값이 복사된다. 즉, 다른 변수에서 해당 값이 변경될 경우 같은 주소를 참조하는 다른 변수들에게도 영향을 끼치는 특징이 있다.

문제는 참조 자료형에서 발생한다. 무엇이 문제일까?


참조 타입 변수와 불변 변수

ES6에서 등장한 const는 불변 변수를 선언할 때 사용한다. 불변 변수란 읽기만 가능하고 수정이 불가능한 변수를 말한다.

const num = 1;
num = 2; // 재할당 불가
// Uncaught TypeError: Assignment to constant variable.

위와 같이 const로 선언된 원시 자료형의 데이터에 재할당을 하면 오류가 발생한다.

하지만 arrayobject와 같은 참조 자료형의 경우 가변 내장 함수(push, pop, shift, ...)를 이용하여 값의 수정이 가능하다.

const arr = [];
arr.push("a");
console.log(arr);
// [ "a" ]

const obj = {};
obj[name] = "kim";
console.log(obj);
// { name: "kim" };

불변 변수를 선언하는 const를 사용하였지만 원시 타입과 다르게 값이 변경된다. 또한 경고 메시지도 발생하지 않는다.

그 이유는 참조 타입 변수가 단지 주소값을 가리키기 때문이다. 내부의 데이터가 변하더라도 그 주소값은 그대로이다. 비유하자면 집 주소는 같은데 사는 사람이 바뀌는 것이다.

이것은 "데이터 무결성(integrity) 제약 조건 위배"에 해당되며 문제를 발생시킬 여지를 남긴다.


참조 타입의 무결성 유지

그럼 참조 타입 변수의 경우 무결성 유지를 어떻게 해줄 수 있을까?

무결성을 유지하려면 기존 데이터를 수정하는 가변 내장 함수를 사용하면 안된다. 그대신 concat, slice, map, filter 등 기존 데이터를 수정(mutate)하지 않고 새로운 데이터를 반환해주는 함수들을 사용해야한다. 이러한 함수들을 무결성 내장 함수라고 한다.

다시 말하자면 참조 타입 변수의 경우 새로운 변수로 unmutate하게 업데이트를 해주어야한다. 새로운 array나 object 등을 반환해주는 함수를 이용하는 것이다.

관련 함수들을 다음과 같이 정리할 수 있다.

가변 내장 함수(mutate)무결성 내장 함수(unmutate)
Addpush, unshift, ...concat, spread operator, ...
Removepop, shift, splice, ...slice, filter, ...
Replacesplice, 재할당, ...map, ...

이 테이블은 대략적인 키워드들만 적어두었으므로 참고만 하면 될 것 같다.


React와 데이터 무결성 상관 관계

리액트와 데이터 무결성이 무슨 관련이 있을까?

리액트는 유저의 인터렉션에 의해 상태 변화를 감지하여 새로고침 없이 페이지를 리렌더링해준다. Virtual DOM과 Real DOM를 비교하여 변동이 생긴 부분만 업데이트를 해주는 방식이다.

그런데 무결성이 지켜지지 않을 경우 데이터에 변동이 생기더라도 해당 변수가 가리키는 주소는 변하지 않는다. 따라서 데이터가 바뀌더라도 리액트는 state이 바뀌었다고 판단하지 못하는 것이다.

따라서 가리키는 주소가 달라지도록 새 변수를 이용해야한다. 그렇다면 값이 같더라도 주소가 바뀌었으므로 리액트는 변화를 감지 할 수 있을 것이다.


비동기적 State 업데이트 고려하기

다음과 같이 state을 업데이트할 경우 또 다른 문제가 발생할 수 있다.

const [fruits, setFruits] = useState(['apple', 'banana']);

setFruits([...fruits, "mango"]);

spread operator를 사용하였으므로 기존 fruits의 데이터는 보존되었고 아마 동작도 될 것이다. 무엇이 문제일까? 이 예시의 경우 문제가 없을 것이다. 하지만 state 업데이트 코드가 여러개일 경우는 다르다.

React는 성능을 위하여 여러개의 setState() 호출을 단일 업데이트로 한꺼번에 처리할 수 있다. 따라서 코드가 복잡해질 경우 state들의 변동이 비동기적으로 업데이트될 가능성이 있다.

예시 코드를 보자.

import { useState } from "react";

const App = () => {
  const [count, setCount] = useState(0);
  
  const countUpHandler = (e) => {
    setCount(count + 2);
  };
  
  const countDownHandler = (e) => {
    setCount(count - 1);
  };
  
  return (
    <>
      <div onClick={countDownHandler}>
        <button onClick={countUpHandler}>Click</button>
      </div>
      <span>{count}</span>
    </>
    )
}

export default App;

버튼 클릭 시 이벤트 버블링이 발생하여 두 핸들러 함수가 모두 실행된다. 그런데 2 - 1 = 1 의 예상과는 다르게 -1씩 count가 감소한다.

왜냐하면 두 setCount() 함수의 인자로 전달되는 count는 0으로 같기 때문이다. 그래서 마지막으로 실행되는 setCount() 함수에 인자로 0이 들어간다. 결국 Virtual DOM에서 최종 count의 결과는 -1이며 최종적으로 setState()은 -1로 업데이트되도록 한번만 호출된다.

연속으로 호출되는 setState()에 매번 새로운 state을 이용하려면 함수 형태의 인자를 넘겨주는 방식으로 구현할 수 있다.

이제 올바르게 참조 자료형 state 업데이트하는 방법을 알아보자.

const [fruits, setFruits] = useState(['apple', 'banana']);

// fruits 업데이트
setFruits(prev => {
  const newFruits = [...prev, "mango"];
  return newFruits;
});

// 축약
setFruits(prev => [...prev, "mango"])

이처럼 setState()의 인자로 함수를 사용하는 형태를 사용하면 안정적으로 리액트가 state 변화를 잘 감지한다. 함수 형태의 인자는 원시 타입에서도 권장된다. 또한 참조 자료형의 unmutate한 업데이트를 하기에도 적합해보인다.

앞으로 setState()을 할 때 웬만하면 인자값을 함수 형태로 주도록 해야겠다.


결론

React 프로젝트를 하며 지금까지는 이유를 잘 모르고 배운대로만 했었다. 강의 자료들이나 블로그 글에서도 자세한 이유는 잘 언급되지 않았었다. 그래서 이번 기회에 여기 저기 분산되어있던 필기, 메모들을 합쳐서 엮어보았다. 자료형 타입, 불변 변수, 무결성 등. 아무튼 이 글이 setState()을 사용하면서 일반적인 방법으로 arrayobject의 업데이트가 왜 안되는지 궁금한 사람들에게 도움이 되었으면한다.

다시 정리하자면,

  1. 참조 타입 자료형의 경우 무결성을 위배하기 쉽다.
  2. 안정적인 리액트 상태관리를 위해 참조 타입의 데이터 무결성을 지켜주어야 한다.
  3. setState() 함수를 사용할 때 항상 함수 형태의 인자를 사용하는것을 권장한다.

참고 : Immer 라는 관련 프레임워크도 존재하는데 불변성을 유지하는 코드를 쉽게 작성하도록 도와준다. 참고


참조 자료

profile
개발 회고록

0개의 댓글