카테고리 없음

[공식문서 읽기]Updating Objects in State

아보카도 있었어! 2023. 8. 21. 04:05

| React에서 state로 관리하고 있는 객체를 업데이트하려면 직접 객체를 수정하는게 아니라 새로운 객체를 만들어 전달해야 한다.(replace)

 

 

리액트로 달력 만들기 예제를 학습하다가

    const date = new Date(nowDate.getTime());

위 코드와

  const [nowDate, setNowDate] = useState<Date>(() => new Date());
  const newDate = nowDate;

아래 코드의 차이를 알 수가 없었다. 그 차이는 `Date.setMonth()` 메서드를 사용한 뒤 상태 변경 함수를 실행한 후에 발생했다.

첫 번째 코드는 리렌더링을 트리거하여 바뀌는 화면을 보여주었지만, 이전 상태 변수 객체를 참조하고 있던 두 번째 코드는 리렌더링을 트리거하지 않는다는 차이점이 있던 것이다.

코드 블럭 내에서 `nowDate`를 참조하는 `newDate`의 메서드는 잘 동작하고, 객체의 값도 바뀌었지만, 바뀐 값을 리액트가 감지하지 못했다.

 

이에 리액트 공식문서를 학습한 내용을 정리해 보았다.

https://react.dev/learn/updating-objects-in-state


What's mutation?

| 숫자, 문자, 불리언과 같은 원시타입들은 불변이지만, 객체 형태의 값은 기술적으로는 객체 자체의 내용 변경이 가능하다.

const [x, setX] = useState(0);

`0`은 원시타입 중 숫자에 해당하기 때문에 그자체의 변경이 불가능하다. 이는 곧 읽기 전용(read-only)을 의미하며 state는 불변이기 때문에, 리렌더를 트리거하기 위해서는 아래와 같이 값을 대체하는 방식을 사용했다.(replace)

setX(5);

 

리액트에서는 객체 형태로도 state를 저장할 수 있으며, 객체형은 그 자체의 값을 변경하는 것이 가능하다.

const [position, setPosition] = useState({ x: 0, y: 0 });
position.x = 5;
console.log(position) // {x: 5, y: 0}

 

Treat state as read-only

| 리액트의 state에 넣은 객체를 변경할 수 있어도 원시타입과 같은 불변처럼 취급해야 한다.

 

위에서 살펴본 것처럼 자바스크립트의 객체는 그 자체로 내용을 변경할 수 있다.

하지만, 원시 타입의 state를 변경했던 것처럼 상태 변경 함수의 인자로 변경할 값을 전달해서 대체해야 한다.

그렇지 않으면 리액트는 객체의 내용이 변경된 것을 인지하지 못한다.


다시 달력 만들기 예제를 따라가 보면,

const [nowDate, setNowDate] = useState(() => new Date());

현재 화면에 렌더링된 캘린더의 연도(year)와 월(month)을 상태변수 `nowDate`로 관리한다.

 

우리가 기대하는 값은 사용자가 이전/다음 월을 조작하는 버튼을 눌렀을 때 화면에 있는 *월이 바뀌는 것이다.

function changeMonth(change) {
    const date = new Date(nowDate.getTime());
    date.setMonth(date.getMonth() + change)
    setNowDate(date)
}
<button onClick={() => {changeMonth(-1);}}>{"<"}</button>        
<button onClick={() => {changeMonth(1);}}>{">"}</button>

함수 changeMonth(change) 는 사용자의 컨트롤 인수(<, >)를 받아 상태변수 `nowDate`를 변경하는 역할을 한다.

'Treat state as read-only'섹션에서 살펴본 것처럼, 상태 변경 함수를 사용해 값을 대체하고 있다.

다음 코드를 한 번 살펴보자.

function changeMonth(change) {
    const date = new Date(nowDate.getTime());
    date.setMonth(date.getMonth() + change)
    setNowDate(date)
}
function changeMonth(change) {
    const date = nowDate;
    date.setMonth(date.getMonth() + change)
    setNowDate(date)
}

위 코드와 아래 코드의 차이는 새로운 Date 객체의 생성 여부 뿐이다. 공식문서에서 살펴본 것처럼 객체를 대체하는 방법을 사용하고 있는 것은 둘다 같다.

 

위 코드를 순서대로 동작시켜 보면,

클릭할 때마다 리렌더 트리거

 

우리가 기대한 값은 사용자가 월을 조작할 때마다 화면이 리렌더링되는 것이다. 하지만 아래의 코드는 컨트롤 버튼을 눌러도 화면이 변경되지 않다가 강제 리렌더링되었을 때, 원하는 값으로 변경되는 것을 볼 수 있다.

즉, 아래 코드는 리액트가 상태 변경 함수에 대체할 값을 전달했음에도 객체의 값이 변경되었음을 인지하지 못했다.

 

이러한 현상의 원인은 객체의 복사와 연관이 있다.

함수 changeMonth의 지역 변수 date 선언부를 다시 살펴보자.

const date = new Date(nowDate.getTime())
const date = nowDate;

아래 코드는 상태 변수 nowDate 객체를 변수 date에 할당했고, 위 코드는 새로운 Date객체를 만들어 할당했다. 자바스크립트의 객체는 복사될 때 기본적으로 얕은 복사가 일어나기 때문에 객체 자체가 아닌 원본 객체의 참조값이 할당된다.

결국 아래 코드의 지역변수 date가 가리키고 있는 객체는 원본 객체이자 이전 렌더링의 상태 nowDate와 동일하므로, 리액트는 새로운 값으로 인지를 하지 못하고, 리렌더링을 트리거하지 않는 것이다. 리액트는 이전 프로퍼티나 상태가 다음 프로퍼티나 상태와 동일한 경우 작업을 건너뛰기 때문이다. 이전 객체와 새로 들어온 객체의 값이 동일하다면, 리액트는 변경되지 않은 것으로 간주하고, 리렌더링을 트리거 하지 않는다.

 

이번 학습을 통해 다시 한번 리액트의 객체형 업데이트 동작 방식을 이해하고, 자바스크립트 객체의 얕은 복사/깊은 복사에 대해 알아볼 수 있는 기회가 되었다.