githubEdit

객체 State 업데이트하기

리액트의 State는 객체를 포함한 모든 종류의 자바스크립트 값을 가질 수 있습니다. 하지만 React state가 가진 객체는 직접 변경해서는 안 됩니다.

객체의 변경(mutation)

const [position, setPosition] = useState<{x: number, y: number}>({x: 0, y: 0});

//객체 자체의 내용 변경
position.x = 5;

React state의 객체들은 이처럼 기술적으로 변경하는 것은 가능하나, 숫자, 불리언, 문자열과 같이 불변성을 가진 것처럼 다뤄야 합니다.

State를 읽기 전용인 것처럼 다루기

state에 저장한 자바스크립트 객체는 어떤 것이라도 읽기 전용인 것처럼 다루어야 합니다.

export default function MovingDot() {
  const [position, setPosition] = useState<{x: number, y: num}>({
    x: 0,
    y: 0
  });
  
  return (
    <div
	    onPointerMove={(e: React.PointerEvent<HTMLDivElement>) => {
        position.x = e.clientX;
        position.y = e.clientY;
      }}
      style={{
        position: 'relative',
        width: '100vw',
        height: '100vh',
      }}>
      <div style={{
        position: 'absolute',
        backgroundColor: 'red',
        borderRadius: '50%',
        transform: `translate(${position.x}px, ${position.y}px)`,
        left: -10,
        top: -10,
        width: 20,
        height: 20,
      }} />
    </div>
  )
}

다음 코드에서 우리는 빨간점이 커서를 따라 이동하기를 원했지만 작동하지 않습니다.

이 코드는 처음 useState()에서 position값이 초기화될 때, 한번 렌더링 됩니다.

하지만 이 후, 아무리 포인터를 이동해서 position값을 직접 변경하더라도 렌더링이 일어나지 않기 때문에 원하는 결과를 볼 수 없습니다.

이를 해결하기 위해서는 State값이 바뀔때마다 렌더링을 일으키는 useState의 set함수를 활용해 수정해야합니다.

다음과 같이 수정한다면, 마우스 커서를 옮길 때마다 렌더링된 빨간색 원을 볼 수 있을 것입니다.

이는 useState()의 작동원리arrow-up-right와 관련이 있습니다.

만약 아예 새로운 객체를 만들어 새로운 객체를 직접 변경후, set함수를 이용해 새로운 객체로 변경한다면 이는 존재하는 객체를 변경하는 것이 아니기 때문에 제대로 작동합니다.

스프레드 문법으로 객체 복사하기

person이라는 객체가 있을때, 이 값을 변경하기 위해서는 set함수를 사용해야합니다.

하지만 단 하나의 값을 바꾸고 싶을때가 있습니다. 위와 같이 기존에 존재하는 다른 데이터를 직접 복사해 구현할 수 있지만 이는 비효율적입니다.

이때 스프레드 문법을 사용할 수 있습니다.

또한 현재는 각각 input에 대한 onChange함수가 따로 존재하지만, input에 name을 지정해 스프레드 문법을 활용하면 이를 간소화시킬 수 있습니다.

이때 e.target.name<input> DOM 엘리먼트의 name 프로퍼티를 나타냅니다.

중첩된 객체에서의 스프레드 문법

스프레드 문법은 한 레벨 깊이의 내용만 복사합니다(얕은 복사). 따라서 만약 중첩된 프로퍼티를 업데이트하고 싶다면 한 번 이상 사용해야 합니다.

예시 코드의 artwork의 값을 변경하기 위해서는 스프레드 문법을 두번 사용해야 합니다.

이 때 중첩되어 보이는 객체들이 실제로 중첩된 것은 아닙니다. 객체들은 Linked List처럼 가리키는 구조로 되어있습니다. (ex. person ↔ artwork)

Immer로 평탄화하기

state가 깊이 중첩되어있을때는 평탄화를 고려할 수도 있습니다. 그리고 이를 위한 라이브러리가 **‘Immer’**입니다.

Immer가 제공하는 draftProxyarrow-up-right라고 하는 특별한 객체 타입으로, 당신이 하는 일을 “기록” 합니다. Immer는 내부적으로 draft의 어느 부분이 변경되었는지 알아내어, 변경사항을 포함한 완전히 새로운 객체를 생성합니다.

Proxy는 JavaScript의 내장 객체로, 객체의 속성 접근, 설정, 삭제 등을 가로채서 커스텀 동작을 정의할 수 있습니다. ImmerProxy를 활용해 객체의 변경사항을 "가로채고 추적"합니다.

이는 로그 형태를 추적하는 방식과는 다르며 Proxy객체 고유의 특성을 이용해 변경 작업을 실시간으로 감지하는 것이라고 합니다.

Immer는 원본 객체를 Proxy로 래핑하여, 읽기/쓰기 작업을 감지합니다.

Proxy는 get, set, deleteProperty 등과 같은 핸들러를 통해 객체의 특정 작업을 가로채서 추가적인 작업을 수행할 수 있습니다.

이를 통해, 어떤 속성이 변경되었는지 실시간으로 추적할 수 있습니다.

"기록"의 구현 방식:

  • Proxy는 직접적으로 객체의 변경 사항을 메모리에 저장하지 않습니다. 대신, 어떤 속성이 읽히거나 쓰였는지를 감지하여 내부적으로 이를 "변경 사항"으로 간주합니다.

  • 변경된 정보를 내부적으로 관리하며, 최종적으로 produce 함수가 끝날 때, 변경 사항을 반영하여 새로운 객체를 생성합니다.

만약 중첩된 객체를 변경하려고 할 경우, 다음은 마치 직접 변경같지만 실제로는 그렇지 않습니다. 하지만 일반적인 변경과 다르게 이전 state를 덮어쓰지 않고 새로운 객체를 생성합니다.

왜 React에서 state 변경은 권장되지 않나요?

  • 디버깅: 만약 console.log를 사용하고 state를 변경하지 않는다면, 과거 로그들은 가장 최근 state 변경 사항들에 의해 지워지지 않습니다. 따라서 state가 렌더링 사이에 어떻게 바뀌었는지 명확하게 알 수 있습니다.

  • 최적화: 보편적인 React 최적화 전략arrow-up-right은 이전 props 또는 state가 다음 것과 동일할 때 일을 건너뛰는 것에 의존합니다. state를 절대 변경하지 않는다면 변경사항이 있었는지 확인하는 작업이 매우 빨라집니다. prevObj === obj를 통해 내부적으로 아무것도 바뀌지 않았음을 확인할 수 있습니다.

  • 새로운 기능: 우리가 만드는 새로운 React 기능들은 스냅샷처럼 다루어지는 것arrow-up-right에 의존합니다. 만약 state의 과거 버전을 변경한다면, 새로운 기능을 사용하지 못할 수 있습니다.

  • 요구사항 변화: 취소/복원 구현, 변화 내역 조회, 사용자가 이전 값으로 폼을 재설정하기 등의 기능은 아무것도 변경되지 않았을 때 더 쉽습니다. 왜냐하면 당신은 메모리에 state의 이전 복사본을 저장하여 적절한 상황에 다시 사용할 수 있기 때문입니다. 변경하는 것으로 시작하게 되면 이러한 기능들은 나중에 추가하기 어려울 수 있습니다.

  • 더 간단한 구현: React는 변경에 의존하지 않기 때문에 객체로 뭔가 특별한 것을 할 필요가 없습니다. 프로퍼티를 가져오거나, 항상 프록시로 감싸거나, 다른 많은 “반응형” 솔루션이 그러듯 초기화 시에 다른 작업을 하지 않아도 됩니다. 이것은 React가 state에 —얼마나 크던— 추가적인 성능 또는 정확성 함정 없이 아무 객체나 넣을 수 있게 해주는 이유이기도 합니다.

⇒ 결국 state를 직접 변경하게 되면 변경사항 확인 등 이전 state값을 활용할 수 없기 때문에 권장되지 않습니다.

깊은 복사를 하기 위한 문법

자바스크립트의 스프레드 문법은 얕은 복사(shallow copy)만 지원합니다. 따라서 깊은 복사(deep copy)를 구현하려면 다른 방법을 사용해야 합니다.

1. structuredClone (최신 브라우저 지원)

ECMAScript에서 제공하는 최신 방법으로, 객체의 깊은 복사를 수행합니다. JSON 문자열화 방식보다 더 강력하며 순환 참조를 포함한 객체도 복사가 가능합니다.

  • 장점: 순환 참조, Date, Map, Set 등의 객체도 복사 가능.

  • 제약: 최신 브라우저에서만 동작.

2. lodashcloneDeep

lodash 라이브러리에서 제공하는 cloneDeep 메서드는 깊은 복사를 위한 안정적이고 인기 있는 방법입니다.

  • 장점: 모든 타입과 복잡한 구조를 지원.

3. JSON 문자열화 방식

객체를 JSON으로 변환한 뒤 다시 파싱하여 깊은 복사를 수행합니다.

  • 장점: 간단하고 라이브러리 없이 사용 가능.

  • 제약:

    • 순환 참조를 포함한 객체는 복사 불가.

    • Date, Set, Map, RegExp와 같은 특수 객체는 올바르게 처리되지 않음.

4. 타사 라이브러리 활용

immer 또는 rfdc(Really Fast Deep Clone) 같은 라이브러리도 깊은 복사를 지원합니다.

Immer는 상태 변경을 처리할 때 불변성을 유지하며 깊은 복사를 자동으로 수행합니다.

rfdc 예제

  • 장점: 매우 빠르고 효율적.

Last updated