State에서 객체 업데이트하기
State는 객체를 포함한 모든 종류의 JavaScript 값을 보유할 수 있습니다. 하지만 React state에 보관된 객체를 직접 변경해서는 안 됩니다. 대신 객체를 업데이트하려면 새로운 객체를 생성하거나(또는 기존 객체의 복사본을 만들고) state를 해당 복사본을 사용하도록 설정해야 합니다.
배울 내용
- React state에서 객체를 올바르게 업데이트하는 방법
- 객체를 변이하지 않고 중첩 객체를 업데이트하는 방법
- 불변성이 무엇이며, 어떻게 깨뜨리지 않을지
- Immer를 사용하여 객체 복사를 덜 반복적으로 만드는 방법
변이란 무엇인가요?
state에는 어떤 종류의 JavaScript 값이든 저장할 수 있습니다.
지금까지 숫자, 문자열, 불리언을 다뤘습니다. 이러한 종류의 JavaScript 값은 "불변"입니다. 즉, 변경할 수 없거나 "읽기 전용"입니다. 값을대체하기 위해 리렌더링을 트리거할 수 있습니다:
statex는 0에서5로 변경되었지만,숫자 0 자체는 변경되지 않았습니다. JavaScript에서 숫자, 문자열, 불리언과 같은 내장 원시 값은 어떤 식으로든 변경할 수 없습니다.
이제 state에 있는 객체를 생각해 보세요:
기술적으로는 객체 자체의내용을 변경하는 것이 가능합니다.이를 변이라고 합니다:
그러나 React state의 객체는 기술적으로는 변경 가능하지만, 숫자, 불리언, 문자열처럼불변인 것처럼 취급해야 합니다. 객체를 변이하는 대신 항상 대체해야 합니다.
state를 읽기 전용으로 취급하기
다시 말해, state에 넣은 모든 JavaScript 객체를읽기 전용으로 취급해야 합니다.
이 예시는 현재 포인터 위치를 나타내기 위해 state에 객체를 보유합니다. 빨간 점은 미리보기 영역을 터치하거나 커서를 이동할 때 움직여야 합니다. 하지만 점은 초기 위치에 머물러 있습니다:
문제는 이 코드 때문입니다.
이 코드는 position에 할당된 객체를이전 렌더링에서수정합니다. 하지만 state 설정 함수를 사용하지 않으면 React는 객체가 변경되었다는 사실을 알지 못합니다. 따라서 React는 아무런 응답을 하지 않습니다. 마치 식사를 이미 마친 후에 주문을 변경하려는 것과 같습니다. state를 변이하는 것이 경우에 따라 작동할 수는 있지만, 권장하지 않습니다. 렌더링 중에 접근할 수 있는 state 값을 읽기 전용으로 취급해야 합니다.
이 경우 실제로리렌더링을 트리거하려면, 새로운 객체를생성하여 state 설정 함수에 전달하세요:
이렇게setPosition을 사용하면 React에게 다음과 같이 말하는 것입니다:
- 이 새로운 객체로
position을 대체하세요 - 그리고 이 컴포넌트를 다시 렌더링하세요
이제 미리보기 영역을 터치하거나 커서를 올리면 빨간 점이 포인터를 따라오는 것을 확인하세요:
전개 구문으로 객체 복사하기
이전 예제에서 position객체는 항상 현재 커서 위치에서 새로 생성됩니다. 하지만 종종, 생성 중인 새 객체의 일부로기존데이터를 포함하고 싶을 때가 있습니다. 예를 들어, 폼에서하나의필드만 업데이트하고 다른 모든 필드의 이전 값은 유지하고 싶을 수 있습니다.
이 입력 필드들은 onChange핸들러가 상태를 변이하기 때문에 작동하지 않습니다:
예를 들어, 이 줄은 이전 렌더링의 상태를 변이합니다:
원하는 동작을 얻는 확실한 방법은 새 객체를 생성하여 setPerson에 전달하는 것입니다. 하지만 여기서는 변경된 필드가 하나뿐이므로 기존 데이터도그 안에 복사하고 싶습니다:
각 속성을 개별적으로 복사할 필요 없이...객체 전개 구문을 사용할 수 있습니다.
이제 폼이 작동합니다!
각 입력 필드에 대해 별도의 상태 변수를 선언하지 않았다는 점에 주목하세요. 큰 폼의 경우, 모든 데이터를 객체로 그룹화하여 유지하는 것은 올바르게 업데이트하기만 한다면 매우 편리합니다!
참고로 ...전개 구문은 "얕은" 복사입니다. 즉, 한 단계 깊이만 복사합니다. 이는 빠르지만, 중첩된 속성을 업데이트하려면 두 번 이상 사용해야 한다는 의미이기도 합니다.
중첩된 객체 업데이트하기
다음과 같은 중첩된 객체 구조를 생각해 보세요:
만약 person.artwork.city를 업데이트하고 싶다면, 변이(mutation)를 사용하면 어떻게 하는지 명확합니다:
하지만 React에서는 상태를 불변으로 취급합니다!city를 변경하려면, 먼저 새로운artwork객체(이전 객체의 데이터로 미리 채워진)를 생성한 다음, 새로운artwork를 가리키는 새로운person객체를 생성해야 합니다:
또는, 단일 함수 호출로 작성하면 다음과 같습니다:
이 방법은 약간 장황하지만 많은 경우에 잘 작동합니다:
Immer로 간결한 업데이트 로직 작성하기
상태가 깊게 중첩되어 있다면, 평탄화하는 것을 고려해 볼 수 있습니다. 하지만 상태 구조를 변경하고 싶지 않다면, 중첩된 전개 연산자에 대한 지름길을 선호할 수 있습니다.Immer는 편리하지만 변이하는 구문을 사용하여 작성할 수 있게 해주고 복사본 생성을 대신 처리해 주는 인기 있는 라이브러리입니다. Immer를 사용하면 작성한 코드가 "규칙을 깨고" 객체를 변이하는 것처럼 보입니다:
하지만 일반적인 변이와 달리, 이전 상태를 덮어쓰지 않습니다!
Immer를 사용해 보려면:
- Immer를 의존성으로 추가하려면
npm install use-immer를 실행하세요. - 그런 다음
import { useState } from 'react'를 로 바꾸세요.import { useImmer } from 'use-immer'
위 예제를 Immer로 변환한 결과는 다음과 같습니다:
이벤트 핸들러가 얼마나 더 간결해졌는지 확인하세요. 하나의 컴포넌트에서useState와 useImmer를 원하는 만큼 자유롭게 혼합하여 사용할 수 있습니다. Immer는 업데이트 핸들러를 간결하게 유지하는 좋은 방법이며, 특히 상태에 중첩이 있고 객체를 복사하는 것이 반복적인 코드를 초래할 때 유용합니다.
요약
- React의 모든 state를 불변으로 취급하세요.
- state에 객체를 저장할 때, 객체를 변이(mutate)하면 렌더링이 트리거되지 않으며 이전 렌더 "스냅샷"의 state가 변경됩니다.
- 객체를 변이하는 대신, 객체의새로운버전을 생성하고, state를 그 버전으로 설정하여 다시 렌더링을 트리거하세요.
- 객체의 복사본을 만들기 위해
{...obj, something: 'newValue'}객체 전개 구문을 사용할 수 있습니다. - 전개 구문은 얕습니다: 한 단계 깊이만 복사합니다.
- 중첩된 객체를 업데이트하려면, 업데이트하는 위치부터 위쪽으로 모든 경로의 복사본을 만들어야 합니다.
- 반복적인 복사 코드를 줄이려면 Immer를 사용하세요.
Try out some challenges
Challenge 1 of 3:Fix incorrect state updates #
This form has a few bugs. Click the button that increases the score a few times. Notice that it does not increase. Then edit the first name, and notice that the score has suddenly “caught up” with your changes. Finally, edit the last name, and notice that the score has disappeared completely.
Your task is to fix all of these bugs. As you fix them, explain why each of them happens.
