본문 바로가기

이것저것 스터디📚/React - 공식 문서

React 공식문서 -Updating Objects in State(객체 state 업데이트)

React state에 있는 객체를 업데이트하려면 직접 객체를 변경하는 것이 아닌, 새 객체를 생성하고(혹은 기존 객체의 복사본을 만들고), 해당 복사본을 사용해야 합니다.


What's a mutation?(변이란 무엇인가요?)

 

JavaScript의 어떤 값이든 state에 저장할 수 있다. 숫자, 문자열, 불리언 값은 "불변", 즉 변이할 수 없거나 "읽기 전용"이기 때문에 다시 렌더링을 촉발하여 값을 바꿀 수 있다.

 

예를들어 기존 state 변수는 0이고 setState(5)를 실행해서 state 변수를 5로 변경했을 때, 숫자 0 자체는 변경되지 않았다.

 

객체의 경우에는 기술적으로 객체 자체의 내용을 변경하는 것은 가능하며 이를 변이(mutation)이라고 부른다. 대신 객체를 직접 변이하는 대신 항상 교체해야한다.


Treat state as read-only(state를 읽기 전용으로 취급하세요)

 

다시 말해 state에 넣는 모든 JavaScript 객체를 읽기 전용으로 취급해야 한다.

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>
  );
}

 

 

만약 위 예제와 같이 객체 자체의 내용을 변경하는 경우에는 React는 객체가 변이되었다는 사실을 알지 못한다. 따라서, 리렌더링을 촉발하려면 새 객체를 생성하고 state 설정자 함수에 전달해야한다.

import { useState } from 'react';
export default function MovingDot() {
  const [position, setPosition] = useState({
    x: 0,
    y: 0
  });
  return (
    <div
      onPointerMove={e => {
        setPosition({
          x: e.clientX,
          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>
  );
}

 

* 지역 변이는 괜찮습니다

예를 들어 방금 생성한 새로운 객체를 변이하는 것은 괜찮다. 변이는 이미 state가 있는 기존 객체를 변경할 때만 문제가 되기 때문이다.

const nextPosition = {};
nextPosition.x = e.clientX;
nextPosition.y = e.clientY;
setPosition(nextPosition);

setPosition({
  x: e.clientX,
  y: e.clientY
});

// 두 방식은 동일한 결과를 반환한다

Copying objects with the spread syntax (전개 구문을 사용하여 객체 복사하기)

 

만약 기존 데이터를 새로 만드는 개체의 일부로 포함시키려고 한다면, 모든 속성을 개별적으로 복사할 필요가 없도록 ... 객체 전개 구문을 사용할 수 있다.

setPerson({
  ...person, // Copy the old fields
             // 이전 필드를 복사합니다.
  firstName: e.target.value // But override this one
                            // 단, first name만 덮어씌웁니다. 
});

대신 ... 전개 구문은 "얕은" 구문으로 한 단계 깊이만 복사한다는 점에 유의해야 한다.

 

* 여러 필드에 단일 이벤트 핸들러 사용하기

객체 내에서 중괄호('[]')를 사용하여 동적 이름을 가진 프로퍼티를 지정할 수도 있다.

import { useState } from 'react';

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

  function handleChange(e) {
    setPerson({
      ...person,
      [e.target.name]: e.target.value
    });
  }

  return (
    <>
      <label>
        First name:
        <input
          name="firstName"
          value={person.firstName}
          onChange={handleChange}
        />
      </label>
      <label>
        Last name:
        <input
          name="lastName"
          value={person.lastName}
          onChange={handleChange}
        />
      </label>
      <label>
        Email:
        <input
          name="email"
          value={person.email}
          onChange={handleChange}
        />
      </label>
      <p>
        {person.firstName}{' '}
        {person.lastName}{' '}
        ({person.email})
      </p>
    </>
  );
}

Updating a nested object(중첩된 객체 업데이트하기)

 

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

위 예제와 같은 중첩된 객체 구조를 변이를 사용하지 않고 업데이트 한다고 가정해보자.

// city를 변경하고자 할때,

// 새로운 artwork 객체(이전 artwork 데이터로 미리 채워진)생성
const nextArtwork = { ...person.artwork, city: 'New Delhi' };

// 새 artwork을 가리키는 새로운 person 객체 생성
const nextPerson = { ...person, artwork: nextArtwork };

setPerson(nextPerson);
// 단일 함수 호출을 사용할 때
setPerson({
  ...person, // Copy other fields
  artwork: { // but replace the artwork
             // 대체할 artwork를 제외한 다른 필드를 복사합니다.
    ...person.artwork, // with the same one
    city: 'New Delhi' // but in New Delhi!
                      // New Delhi'는 덮어씌운 채로 나머지 artwork 필드를 복사합니다!
  }
});

 

* 객체는 실제로 중첩되지 않습니다

let obj1 = {
  title: 'Blue Nana',
  city: 'Hamburg',
  image: 'https://i.imgur.com/Sd1AgUOm.jpg',
};

let obj2 = {
  name: 'Niki de Saint Phalle',
  artwork: obj1
};

let obj3 = {
  name: 'Copycat',
  artwork: obj1
};

위 예제와 같이 obj1은 obj2 내부에 있지 않기 때문에, obj3도 obj1을 가리킬수 있다. 즉, 코드 상에 중첩되게 표현되는 중첩객체는 실은 프로퍼티를 사용하여 서로를 가리키는 별도의 객체이다.


Write concise update logic with Immer(Immer로 간결한 업데이트 로직 작성)

 

state가 깊게 중첩된 경우에는 평평하게 만드는 것을 고려할 수 있는데, 만약 state 구조를 변경하고 싶지 않다면 중첩된 전개 구문보다 I편리하게 변이 구문을 사용하여 작성하더라도 자동으로 사본을 생성해주는 Immer 라이브러리를 사용할 수 있다. (하지만 일반 변이와 달리 이전 state를 덮어쓰지는 않는다.)

updatePerson(draft => {
  draft.artwork.city = 'Lagos';
});

 

* Immer는 어떻게 동작 하나요?

Immer에서 제공하는 draft는 프록시라는 특수한 유형의 객체로 사용자가 수행하는 작업을 기록한다. Immer는 내부적으로 draft의 어떤 부분이 변경되었는지 파악하고 편집 내용이 포함된 완전한 새로운 객체를 생성한다.

 

* Immer 사용 예시

// import { useState } from 'react' 대신 사용
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;
    });
  }

  return (
    <>
      <label>
        Name:
        <input
          value={person.name}
          onChange={handleNameChange}
        />
      </label>
      <label>
        Title:
        <input
          value={person.artwork.title}
          onChange={handleTitleChange}
        />
      </label>
      <label>
        City:
        <input
          value={person.artwork.city}
          onChange={handleCityChange}
        />
      </label>
      <label>
        Image:
        <input
          value={person.artwork.image}
          onChange={handleImageChange}
        />
      </label>
      <p>
        <i>{person.artwork.title}</i>
        {' by '}
        {person.name}
        <br />
        (located in {person.artwork.city})
      </p>
      <img 
        src={person.artwork.image} 
        alt={person.artwork.title}
      />
    </>
  );
}

* 참고 : React 공식문서(https://react-ko.dev/learn)