React 공식문서 이해하기 (14)

Syoee·2023년 11월 22일
0

React

목록 보기
14/30
post-thumbnail

Chapter 2. Adding Interactivity

#6 객체 state 업데이트

학습 목차

1. 변이란 무엇인가?
2. state를 읽기 전용으로 취급하라
3. 전개 구문을 사용하여 객체 복사하기
4. 중첩된 객체 업데이트하기


1. state 업데이트 일괄처리

  • 어떤 종류의 JavaScript 값이든 state에 저장할 수 있다.

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

  • 지금까지 숫자, 문자열, 불리언값으로 작업해 보았다.
    이러한 종류의 JavaScript 값은 “불변”, 즉,변이할 수 없거나 “읽기 전용”이다.
    다시 렌더링을 트리거하여 값을 바꿀 수 있다.

    setX(5);

  • x state가 0에서 5로 변경 되었지만 숫자 0 자체는 변경되지 않았다.
    JavaScript에서는 숫자, 문자열, 불리언과 같은 빌트인 원시 자료형 값을 변경할 수 없다.

  • 객체 state를 살펴보면

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

  • 기술적으로 객체 자체의 내용을 변경하는 것은 가능하다.
    이를 변이(mutation) 라고 한다.

    position.x = 5;

  • React state의 객체는 기술적으로는 변이할 수 있지만, 숫자, 불리언(boolean), 문자열과 같이 불변하는 것처럼 취급해야 한다.
    객체를 직접 변이하는 대신, 항상 교체해야 한다.

2. state를 읽기 전용으로 취급하라

  • 다시 말해 state에 넣는 모든 JavaScript 객체를 읽기 전용으로 취급해야 한다.
  • 아래 예제에서는 현재 포인터 위치를 나타내는 state 객체가 있다.
  • 미리 보기 영역 위로 커서를 터치하거나 이동하면 빨간색 점이 움직여야 한다.
  • 그러나 점은 초기 위치에 유지되고 있다.
import { useState } from 'react';
export default function MovingDot() {
  const [position, setPosition] = useState({
    x: 0,
    y: 0
  });
  return (
    <div
      onPointerMove={e => {
        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>
  );
}
onPointerMove={e => {
  position.x = e.clientX;
  position.y = e.clientY;
}}
  • 이 코드는 이전 렌더링에서 position에 할당된 객체를 수정한다.
    하지만 state 설정자 함수를 사용하지 않으면 React는 객체가 변이되었다는 사실을 알지 못한다.
    그래서 React는 아무 반응도 하지 않는다.

  • 이미 음식을 다 먹은 후에 주문을 바꾸려고 하는 것과 같다.

  • state 변이는 경우에 따라 작동할 수 있지만 권장하지 않는다.

  • 렌더링에서 접근할 수 있는 state 값은 읽기 전용으로 취급해야 합니다.

  • 이 경우 실제로 리렌더링을 촉발하려면 새 객체를 생성하고 state 설정자 함수에 전달하라.

onPointerMove={e => {
  setPosition({
    x: e.clientX,
    y: e.clientY
  });
}}
  • setPosition으로 React에 다음을 지시한다
    • position을 이 새 객체로 바꿔라.
    • 이 컴포넌트를 다시 렌더링 하라.

3. 전개 구문을 사용하여 객체 복사하기

  • 아래의 입력 필드는 onChange 핸들러가 state를 변이하기 때문에 작동하지 않는다.
import { useState } from 'react';

export default function Form() {
  const [person, setPerson] = useState({
    firstName: 'Barbara',
    lastName: 'Hepworth',
    email: 'bhepworth@sculpture.com'
  });

  function handleFirstNameChange(e) {
    person.firstName = e.target.value;
  }

  function handleLastNameChange(e) {
    person.lastName = e.target.value;
  }

  function handleEmailChange(e) {
    person.email = e.target.value;
  }

  return (
    <>
      <label>
        First name:
        <input
          value={person.firstName}
          onChange={handleFirstNameChange}
        />
      </label>
      <label>
        Last name:
        <input
          value={person.lastName}
          onChange={handleLastNameChange}
        />
      </label>
      <label>
        Email:
        <input
          value={person.email}
          onChange={handleEmailChange}
        />
      </label>
      <p>
        {person.firstName}{' '}
        {person.lastName}{' '}
        ({person.email})
      </p>
    </>
  );
}
  • 예를 들어, 이 줄은 이전 렌더링 할때의 state를 변이한다.

    person.firstName = e.target.value;

  • 원하는 동작을 얻을 수 있는 가장 안정적인 방법은 새 객체를 생성하고 이를 setPerson에 전달하는 것이다.
    하지만 여기서는 필드 중 하나만 변경되었으므로 기존 데이터도 복사하고 싶을 때

    setPerson({
     firstName: e.target.value, // New first name from the input
                                // input에서 받아온 새로운 first name 
     lastName: person.lastName,
     email: person.email
    });
  • 모든 속성을 개별적으로 복사할 필요가 없도록 ... 객체 전개 구문을 사용할 수 있다.

    setPerson({
     ...person, // Copy the old fields
                // 이전 필드를 복사합니다.
     firstName: e.target.value // But override this one
                               // 단, first name만 덮어씌웁니다. 
    });
  • 각 입력 필드에 대해 별도의 state 변수를 선언하지 않은 것을 주목하자.

  • 큰 양식의 경우 올바르게 업데이트하기만 하면 모든 데이터를 객체에 그룹화하여 보관하는 것이 매우 편리하다.

    import { useState } from 'react';
    
    export default function Form() {
     const [person, setPerson] = useState({
       firstName: 'Barbara',
       lastName: 'Hepworth',
       email: 'bhepworth@sculpture.com'
     });
    
     function handleFirstNameChange(e) {
       setPerson({
         ...person,
         firstName: e.target.value
       });
     }
    
     function handleLastNameChange(e) {
       setPerson({
         ...person,
         lastName: e.target.value
       });
     }
    
     function handleEmailChange(e) {
       setPerson({
         ...person,
         email: e.target.value
       });
     }
    
     return (
       <>
         <label>
           First name:
           <input
             value={person.firstName}
             onChange={handleFirstNameChange}
           />
         </label>
         <label>
           Last name:
           <input
             value={person.lastName}
             onChange={handleLastNameChange}
           />
         </label>
         <label>
           Email:
           <input
             value={person.email}
             onChange={handleEmailChange}
           />
         </label>
         <p>
           {person.firstName}{' '}
           {person.lastName}{' '}
           ({person.email})
         </p>
       </>
     );
    }
    
  • ... 전개 구문은 “얕은” 구문으로, 한 단계 깊이만 복사한다는 점에 유의하자.

  • 속도는 빠르지만 중첩된 프로퍼티를 업데이트하려면 두 번 이상 사용해야 한다는 뜻이기도 하다.

4. 중첩된 객체 업데이트하기

4-1. Immer로 간결한 업데이트 로직 작성

  • state가 깊게 중첩된 경우 그것을 평평하게 만드는 것을 고려할 수 있다.
    하지만 state 구조를 변경하고 싶지 않다면 중첩된 전개 구문보다 더 간편한 방법을 선호할 수 있다.
    Immer는 변이 구문을 사용하여 작성하더라도 자동으로 사본을 생성해주는 편리한 인기 라이브러리이다.
    Immer를 사용하면 작성하는 코드가 “규칙을 깨고” 객체를 변이하는 것처럼 보인다.
updatePerson(draft => {
  draft.artwork.city = 'Lagos';
});
  • 하지만 일반 변이와 달리 이전 state를 덮어쓰지 않는다.

  • Immer를 사용해 보려면

    • npm install use-immer를 실행하여 Immer를 의존성으로 추가합니다.
    • 그런 다음 import { useState } from 'react'import { useImmer } from 'use-immer'로 바꿉니다.
  • 아래는 위의 예제를 Immer로 변환한 것이다.

import { useImmer } from 'use-immer';

export default function Form() {
  const [person, updatePerson] = useImmer({
    name: 'Niki de Saint Phalle',
    artwork: {
      title: 'Blue Nana',
      city: 'Hamburg',
      image: 'https://i.imgur.com/Sd1AgUOm.jpg',
    }
  });

  function handleNameChange(e) {
    updatePerson(draft => {
      draft.name = e.target.value;
    });
  }

  function handleTitleChange(e) {
    updatePerson(draft => {
      draft.artwork.title = e.target.value;
    });
  }

  function handleCityChange(e) {
    updatePerson(draft => {
      draft.artwork.city = e.target.value;
    });
  }

  function handleImageChange(e) {
    updatePerson(draft => {
      draft.artwork.image = e.target.value;
    });
  }
  • 이벤트 핸들러가 얼마나 간결해졌는지 주목하라.
  • 단일 컴포넌트에서 useStateuseImmer를 원하는 만큼 맞춰 사용할 수 있다.
    특히 state에 중첩이 있고 객체를 복사하면 코드가 반복되는 경우 업데이트 핸들러를 간결하게 유지하는 데 Immer는 좋은 방법이다.

요약

  • React의 모든 state를 불변으로 취급하자.
  • state에 객체를 저장하면 객체를 변이해도 렌더링을 촉발하지 않고 이전 렌더링 “스냅샷”의 state가 변경된다.
  • 객체를 변이하는 대신 객체의 새로운 버전을 생성하고 state를 설정하여 다시 렌더링을 트리거한다.
  • 객체 전개 구문 {...obj, something: 'newValue'}를 사용하여 객체 사본을 만들 수 있다.
  • 전개 구문은 한 수준 깊이만 복사하는 얕은 구문이다.
  • 중첩된 객체를 업데이트하려면 업데이트하려는 위치에서 가장 위쪽까지 복사본을 만들어야 한다.
  • 반복적인 코드 복사를 줄이려면 Immer를 사용하라.

React 공식 문서

https://react.dev/

React 비공식 번역 문서

https://react-ko.dev/

MDN

https://developer.mozilla.org/ko/

Wikipedia

https://ko.wikipedia.org/wiki/

profile
함께 일하고 싶어지는 동료, 프론트엔드 개발자입니다.

0개의 댓글

관련 채용 정보