Updating Objects in State객체 state 업데이트

State can hold any kind of JavaScript value, including objects. But you shouldn’t change objects that you hold in the React state directly. Instead, when you want to update an object, you need to create a new one (or make a copy of an existing one), and then set the state to use that copy. state는 객체를 포함해서, 어떤 종류의 JavaScript 값이든 저장할 수 있습니다. 하지만 React state에 있는 객체를 직접 변이해서는 안 됩니다. 대신 객체를 업데이트하려면 새 객체를 생성하고(혹은 기존 객체의 복사본을 만들고), 해당 복사본을 사용하도록 state를 설정해야 합니다.

You will learn학습 내용

  • How to correctly update an object in React state
  • How to update a nested object without mutating it
  • What immutability is, and how not to break it
  • How to make object copying less repetitive with Immer
  • React state에서 객체를 올바르게 업데이트하는 방법
  • 중첩된 객체를 변이하지 않고 업데이트하는 방법
  • 불변성이란 무엇이며, 불변성을 깨뜨리지 않는 방법
  • Immer로 객체 복사를 덜 반복적으로 만드는 방법

What’s a mutation? 변이란 무엇인가요?

You can store any kind of JavaScript value in state. 어떤 종류의 JavaScript 값이든 state에 저장할 수 있습니다.

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

So far you’ve been working with numbers, strings, and booleans. These kinds of JavaScript values are “immutable”, meaning unchangeable or “read-only”. You can trigger a re-render to replace a value: 지금까지 숫자, 문자열, 불리언값으로 작업해 보았습니다. 이러한 종류의 JavaScript 값은 “불변”, 즉,변이할 수 없거나 “읽기 전용”입니다. 다시 렌더링을 촉발하여 값을 바꿀 수 있습니다:

setX(5);

The x state changed from 0 to 5, but the number 0 itself did not change. It’s not possible to make any changes to the built-in primitive values like numbers, strings, and booleans in JavaScript. x state가 0에서 5로 변경 되었지만 숫자 0 자체는 변경되지 않았습니다. JavaScript에서는 숫자, 문자열, 불리언과 같은 빌트인 원시 자료형 값을 변경할 수 없습니다.

Now consider an object in state: 객체 state를 살펴봅시다:

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

Technically, it is possible to change the contents of the object itself. This is called a mutation: 기술적으로 객체 자체의 내용을 변경하는 것은 가능합니다. 이를 변이(mutation)라고 합니다:

position.x = 5;

However, although objects in React state are technically mutable, you should treat them as if they were immutable—like numbers, booleans, and strings. Instead of mutating them, you should always replace them. React state의 객체는 기술적으로는 변이할 수 있지만, 숫자, 불리언(boolean), 문자열과 같이 불변하는 것처럼 취급해야 합니다. 객체를 직접 변이하는 대신, 항상 교체해야 합니다.

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

In other words, you should treat any JavaScript object that you put into state as read-only. 다시 말해 state에 넣는 모든 JavaScript 객체를 읽기 전용으로 취급해야 합니다.

This example holds an object in state to represent the current pointer position. The red dot is supposed to move when you touch or move the cursor over the preview area. But the dot stays in the initial position: 이 예제에서는 현재 포인터 위치를 나타내는 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>
  );
}

The problem is with this bit of code. 문제는 아래 코드에 있습니다.

onPointerMove={e => {
position.x = e.clientX;
position.y = e.clientY;
}}

This code modifies the object assigned to position from the previous render. But without using the state setting function, React has no idea that object has changed. So React does not do anything in response. It’s like trying to change the order after you’ve already eaten the meal. While mutating state can work in some cases, we don’t recommend it. You should treat the state value you have access to in a render as read-only. 이 코드는 이전 렌더링에서 position에 할당된 객체를 수정합니다. 하지만 state 설정자 함수를 사용하지 않으면 React는 객체가 변이되었다는 사실을 알지 못합니다. 그래서 React는 아무 반응도 하지 않습니다. 이미 음식을 다 먹은 후에 주문을 바꾸려고 하는 것과 같습니다. state 변이는 경우에 따라 작동할 수 있지만 권장하지 않습니다. 렌더링에서 접근할 수 있는 state 값은 읽기 전용으로 취급해야 합니다.

To actually trigger a re-render in this case, create a new object and pass it to the state setting function: 이 경우 실제로 리렌더링을 촉발하려면 새 객체를 생성하고 state 설정자 함수에 전달하세요:

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

With setPosition, you’re telling React: setPosition으로 React에 다음을 지시합니다:

  • Replace position with this new object
  • And render this component again
  • position을 이 새 객체로 바꿔라.
  • 이 컴포넌트를 다시 렌더링 하라.

Notice how the red dot now follows your pointer when you touch or hover over the preview area: 이제 미리보기 영역을 터치하거나 마우스를 가져가면 빨간색 점이 포인터를 따라다니는 것을 확인할 수 있습니다:

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

Deep Dive | 심층 탐구

Local mutation is fine 지역 변이는 괜찮습니다

Code like this is a problem because it modifies an existing object in state: 이와 같은 코드는 기존 객체의 state를 수정하기 때문에 문제가 됩니다:

position.x = e.clientX;
position.y = e.clientY;

But code like this is absolutely fine because you’re mutating a fresh object you have just created: 그러나 이와 같은 코드는 방금 생성한 새로운 객체를 변이하는 것이기 때문에 완전히 괜찮습니다:

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

In fact, it is completely equivalent to writing this: 사실 이렇게 작성하는 것과 완전히 동일합니다:

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

Mutation is only a problem when you change existing objects that are already in state. Mutating an object you’ve just created is okay because no other code references it yet. Changing it isn’t going to accidentally impact something that depends on it. This is called a “local mutation”. You can even do local mutation while rendering. Very convenient and completely okay! 변이는 이미 state가 있는 기존 객체를 변경할 때만 문제가 됩니다. 방금 생성한 객체를 변경해도 다른 코드가 아직 참조하지 않으므로 괜찮습니다. 객체를 변경해도 해당 객체에 의존하는 다른 객체에 실수로 영향을 미치지 않습니다. 이를 “지역 변이(local mutation)“라고 합니다. 렌더링하는 동안에도 지역 변이를 수행할 수 있습니다. 매우 편리하고 완전 괜찮습니다!

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

In the previous example, the position object is always created fresh from the current cursor position. But often, you will want to include existing data as a part of the new object you’re creating. For example, you may want to update only one field in a form, but keep the previous values for all other fields. 이전 예제에서 position 객체는 항상 현재 커서 위치에서 새로 만들어졌습니다. 그러나 종종 기존 데이터를 새로 만드는 객체의 일부로 포함시키고 싶을 때가 있습니다. 예를 들어, form에 있는 하나의 필드만 업데이트하고 다른 모든 필드는 이전 값을 유지하고 싶을 수 있습니다.

These input fields don’t work because the onChange handlers mutate the state: 이러한 입력 필드는 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>
    </>
  );
}

For example, this line mutates the state from a past render: 예를 들어, 이 줄은 이전 렌더링시의 state를 변이합니다:

person.firstName = e.target.value;

The reliable way to get the behavior you’re looking for is to create a new object and pass it to setPerson. But here, you want to also copy the existing data into it because only one of the fields has changed: 원하는 동작을 얻을 수 있는 가장 안정적인 방법은 새 객체를 생성하고 이를 setPerson에 전달하는 것입니다. 하지만 여기서는 필드 중 하나만 변경되었으므로 기존 데이터도 복사하고 싶습니다:

setPerson({
firstName: e.target.value, // New first name from the input
// input에서 받아온 새로운 first name
lastName: person.lastName,
email: person.email
});

You can use the ... object spread syntax so that you don’t need to copy every property separately. 모든 속성을 개별적으로 복사할 필요가 없도록 ... 객체 전개 구문을 사용할 수 있습니다.

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

Now the form works! 이제 폼이 동작합니다!

Notice how you didn’t declare a separate state variable for each input field. For large forms, keeping all data grouped in an object is very convenient—as long as you update it correctly! 각 입력 필드에 대해 별도의 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>
    </>
  );
}

Note that the ... spread syntax is “shallow”—it only copies things one level deep. This makes it fast, but it also means that if you want to update a nested property, you’ll have to use it more than once. ... 전개 구문은 “얕은” 구문으로, 한 단계 깊이만 복사한다는 점에 유의하세요. 속도는 빠르지만 중첩된 프로퍼티를 업데이트하려면 두 번 이상 사용해야 한다는 뜻이기도 합니다.

Deep Dive | 심층 탐구

Using a single event handler for multiple fields 여러 필드에 단일 이벤트 핸들러 사용하기

You can also use the [ and ] braces inside your object definition to specify a property with dynamic name. Here is the same example, but with a single event handler instead of three different ones: 객체 내에서 [] 중괄호를 사용하여 동적 이름을 가진 프로퍼티를 지정할 수도 있습니다. 다음은 동일한 예시이지만 세 개의 다른 이벤트 핸들러 대신 단일 이벤트 핸들러를 사용한 예시입니다:

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

Here, e.target.name refers to the name property given to the <input> DOM element. 여기서 e.target.name<input> DOM 요소에 지정된 name 속성을 참조합니다.

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

Consider a nested object structure like this: 다음과 같은 중첩된 객체 구조를 생각해 보세요

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

If you wanted to update person.artwork.city, it’s clear how to do it with mutation: person.artwork.city를 업데이트하려면 변이를 사용하여 업데이트하는 방법이 명확합니다:

person.artwork.city = 'New Delhi';

But in React, you treat state as immutable! In order to change city, you would first need to produce the new artwork object (pre-populated with data from the previous one), and then produce the new person object which points at the new artwork: 하지만 React에서는 state를 불변으로 취급합니다! city를 변경하려면 먼저 새 artwork 객체(이전 artwork의 데이터로 미리 채워진)를 생성한 다음 새 artwork을 가리키는 새로운 사람 객체를 생성해야 합니다:

const nextArtwork = { ...person.artwork, city: 'New Delhi' };
const nextPerson = { ...person, artwork: nextArtwork };
setPerson(nextPerson);

Or, written as a single function call: 또는 단일 함수 호출로 작성할 수도 있습니다:

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 필드를 복사합니다!
}
});

This gets a bit wordy, but it works fine for many cases: 약간 장황해지긴 하지만 대부분의 경우 잘 작동합니다:

import { useState } from 'react';

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

  function handleNameChange(e) {
    setPerson({
      ...person,
      name: e.target.value
    });
  }

  function handleTitleChange(e) {
    setPerson({
      ...person,
      artwork: {
        ...person.artwork,
        title: e.target.value
      }
    });
  }

  function handleCityChange(e) {
    setPerson({
      ...person,
      artwork: {
        ...person.artwork,
        city: e.target.value
      }
    });
  }

  function handleImageChange(e) {
    setPerson({
      ...person,
      artwork: {
        ...person.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}
      />
    </>
  );
}

Deep Dive | 심층 탐구

Objects are not really nested 객체는 실제로 중첩되지 않습니다

An object like this appears “nested” in code: 이와 같은 객체는 코드에서 “중첩”되어 나타납니다:

let obj = {
name: 'Niki de Saint Phalle',
artwork: {
title: 'Blue Nana',
city: 'Hamburg',
image: 'https://i.imgur.com/Sd1AgUOm.jpg',
}
};

However, “nesting” is an inaccurate way to think about how objects behave. When the code executes, there is no such thing as a “nested” object. You are really looking at two different objects: 그러나 “중첩”은 객체의 동작 방식을 고려해보자면 정확한 방식은 아닙니다. 코드가 실행될 때 “중첩된” 객체 같은 것은 존재하지 않습니다. 실제로는 서로 다른 두 개의 객체를 보고 있는 것입니다:

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

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

The obj1 object is not “inside” obj2. For example, obj3 could “point” at obj1 too: obj1obj2의 “내부”에 있지 않습니다. 예를 들어, obj3obj1을 “가리킬” 수 있습니다:

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

If you were to mutate obj3.artwork.city, it would affect both obj2.artwork.city and obj1.city. This is because obj3.artwork, obj2.artwork, and obj1 are the same object. This is difficult to see when you think of objects as “nested”. Instead, they are separate objects “pointing” at each other with properties. obj3.artwork.city를 변이하면 obj2.artwork.cityobj1.city 모두에 영향을 미칩니다. obj3.artwork, obj2.artwork, obj1이 동일한 객체이기 때문입니다. 객체를 “중첩된” 객체라고 생각하면 이 점을 이해하기 어렵습니다. 실은 프로퍼티를 사용하여 서로를 “가리키는” 별도의 객체입니다.

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

If your state is deeply nested, you might want to consider flattening it. But, if you don’t want to change your state structure, you might prefer a shortcut to nested spreads. Immer is a popular library that lets you write using the convenient but mutating syntax and takes care of producing the copies for you. With Immer, the code you write looks like you are “breaking the rules” and mutating an object: state가 깊게 중첩된 경우 그것을 평평하게 만드는 것을 고려할 수 있습니다. 하지만 state 구조를 변경하고 싶지 않다면 중첩된 전개 구문보다 더 간편한 방법을 선호할 수 있습니다. Immer는 변이 구문을 사용하여 작성하더라도 자동으로 사본을 생성해주는 편리한 인기 라이브러리입니다. Immer를 사용하면 작성하는 코드가 “규칙을 깨고” 객체를 변이하는 것처럼 보입니다:

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

But unlike a regular mutation, it doesn’t overwrite the past state! 하지만 일반 변이와 달리 이전 state를 덮어쓰지 않습니다!

Deep Dive | 심층 탐구

How does Immer work? Immer는 어떻게 동작하나요?

The draft provided by Immer is a special type of object, called a Proxy, that “records” what you do with it. This is why you can mutate it freely as much as you like! Under the hood, Immer figures out which parts of the draft have been changed, and produces a completely new object that contains your edits.

Immer에서 제공하는 draft프록시라는 특수한 유형의 객체로, 사용자가 수행하는 작업을 “기록”합니다. 그렇기 때문에 원하는 만큼 자유롭게 수정할 수 있습니다! Immer는 내부적으로 draft의 어떤 부분이 변경되었는지 파악하고 편집 내용이 포함된 완전히 새로운 객체를 생성합니다.

To try Immer: Immer를 사용해 보려면:

  1. Run npm install use-immer to add Immer as a dependency
  2. Then replace import { useState } from 'react' with import { useImmer } from 'use-immer'
  1. npm install use-immer를 실행하여 Immer를 의존성으로 추가합니다.
  2. 그런 다음 import { useState } from 'react'import { useImmer } from 'use-immer'로 바꿉니다.

Here is the above example converted to Immer: 다음은 위의 예제를 Immer로 변환한 것입니다:

{
  "dependencies": {
    "immer": "1.7.3",
    "react": "latest",
    "react-dom": "latest",
    "react-scripts": "latest",
    "use-immer": "0.5.1"
  },
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test --env=jsdom",
    "eject": "react-scripts eject"
  },
  "devDependencies": {}
}

Notice how much more concise the event handlers have become. You can mix and match useState and useImmer in a single component as much as you like. Immer is a great way to keep the update handlers concise, especially if there’s nesting in your state, and copying objects leads to repetitive code. 이벤트 핸들러가 얼마나 간결해졌는지 주목하세요. 단일 컴포넌트에서 useStateuseImmer를 원하는 만큼 맞춰 사용할 수 있습니다. 특히 state에 중첩이 있고 객체를 복사하면 코드가 반복되는 경우 업데이트 핸들러를 간결하게 유지하는 데 Immer는 좋은 방법입니다.

Deep Dive | 심층 탐구

There are a few reasons: 몇 가지 이유가 있습니다:

  • Debugging: If you use console.log and don’t mutate state, your past logs won’t get clobbered by the more recent state changes. So you can clearly see how state has changed between renders. 디버깅: console.log를 사용하고 state를 변이하지 않으면, 과거의 기록이 최근 state 변이에 의해 지워지지 않습니다. 따라서 렌더링 사이에 state가 어떻게 변경되었는지 명확하게 확인할 수 있습니다.

  • Optimizations: Common React optimization strategies rely on skipping work if previous props or state are the same as the next ones. If you never mutate state, it is very fast to check whether there were any changes. If prevObj === obj, you can be sure that nothing could have changed inside of it. 최적화: 일반적인 React 최적화 전략은 이전 프로퍼티나 state가 다음 프로퍼티나 state와 동일한 경우 작업을 건너뛰는 것에 의존합니다. state를 변이하지 않는다면 변경이 있었는지 확인하는 것이 매우 빠릅니다. 만약 prevObj === obj 라면, 내부에 변경된 것이 없다는 것을 확신할 수 있습니다.

  • New Features: The new React features we’re building rely on state being treated like a snapshot. If you’re mutating past versions of state, that may prevent you from using the new features. 새로운 기능: 우리가 개발 중인 새로운 React 기능은 state가 스냅샷처럼 취급되는 것에 의존합니다. 과거 버전의 state를 변이하는 경우 새로운 기능을 사용하지 못할 수 있습니다.

  • Requirement Changes: Some application features, like implementing Undo/Redo, showing a history of changes, or letting the user reset a form to earlier values, are easier to do when nothing is mutated. This is because you can keep past copies of state in memory, and reuse them when appropriate. If you start with a mutative approach, features like this can be difficult to add later on. 요구 사항 변경: 실행 취소/다시 실행 구현, 변경 내역 표시, 사용자가 양식을 이전 값으로 재설정할 수 있도록 하는 것과 같은 일부 애플리케이션 기능은 아무것도 변이되지 않은 state에서 더 쉽게 수행할 수 있습니다. 과거의 state 복사본을 메모리에 보관하고 필요할 때 재사용할 수 있기 때문입니다. 변경 접근 방식으로 시작하면 나중에 이와 같은 기능을 추가하기 어려울 수 있습니다.

  • Simpler Implementation: Because React does not rely on mutation, it does not need to do anything special with your objects. It does not need to hijack their properties, always wrap them into Proxies, or do other work at initialization as many “reactive” solutions do. This is also why React lets you put any object into state—no matter how large—without additional performance or correctness pitfalls. 더 간단한 구현: React는 변이에 의존하지 않기 때문에 객체에 특별한 작업을 할 필요가 없습니다. 많은 “반응형” 솔루션처럼 프로퍼티를 가로채거나, 항상 프록시로 감싸거나, 초기화할 때 다른 작업을 할 필요가 없습니다. 이것이 바로 React를 사용하면 추가 성능이나 정확성의 함정 없이 아무리 큰 객체라도 state에 넣을 수 있는 이유이기도 합니다.

In practice, you can often “get away” with mutating state in React, but we strongly advise you not to do that so that you can use new React features developed with this approach in mind. Future contributors and perhaps even your future self will thank you! 실제로는 React에서 state를 변이해서라도 잘 “빠져나갈” 수 있겠지만, state의 불변성을 유지하는 접근 방식을 염두에 두고 개발된 새로운 React 기능을 잘 사용할 수 있기 위해서, 그렇게 하지 말 것을 강력히 권장합니다. 미래의 기여자들과 여러분의 미래의 자신도 고마워할 것입니다!

Recap요약

  • Treat all state in React as immutable.
  • When you store objects in state, mutating them will not trigger renders and will change the state in previous render “snapshots”.
  • Instead of mutating an object, create a new version of it, and trigger a re-render by setting state to it.
  • You can use the {...obj, something: 'newValue'} object spread syntax to create copies of objects.
  • Spread syntax is shallow: it only copies one level deep.
  • To update a nested object, you need to create copies all the way up from the place you’re updating.
  • To reduce repetitive copying code, use Immer.
  • React의 모든 state를 불변으로 취급하세요.
  • state에 객체를 저장하면 객체를 변이해도 렌더링을 촉발하지 않고 이전 렌더링 “스냅샷”의 state가 변경됩니다.
  • 객체를 변이하는 대신 객체의 새로운 버전을 생성하고 state를 설정하여 다시 렌더링을 촉발하세요.
  • 객체 전개 구문 {...obj, something: 'newValue'}를 사용하여 객체 사본을 만들 수 있습니다.
  • 전개 구문은 한 수준 깊이만 복사하는 얕은 구문입니다.
  • 중첩된 객체를 업데이트하려면 업데이트하려는 위치에서 가장 위쪽까지 복사본을 만들어야 합니다.
  • 반복적인 코드 복사를 줄이려면 Immer를 사용하세요.

Challenge 1 of 3: Fix incorrect state updates 잘못된 state 업데이트 수정하기

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. 이 form에는 몇 가지 버그가 있습니다. 점수를 올리는 버튼을 몇 번 클릭해 보세요. 점수가 올라가지 않는 것을 확인하세요. 그런 다음 이름을 수정하면 점수가 갑자기 변경 사항을 “따라잡는” 것을 확인할 수 있습니다. 마지막으로 last name을 수정하면 점수가 완전히 사라진 것을 확인할 수 있습니다.

Your task is to fix all of these bugs. As you fix them, explain why each of them happens. 여러분의 임무는 이 모든 버그를 수정하는 것입니다. 버그를 수정하면서 각각의 버그가 발생한 이유를 설명하세요.

import { useState } from 'react';

export default function Scoreboard() {
  const [player, setPlayer] = useState({
    firstName: 'Ranjani',
    lastName: 'Shettar',
    score: 10,
  });

  function handlePlusClick() {
    player.score++;
  }

  function handleFirstNameChange(e) {
    setPlayer({
      ...player,
      firstName: e.target.value,
    });
  }

  function handleLastNameChange(e) {
    setPlayer({
      lastName: e.target.value
    });
  }

  return (
    <>
      <label>
        Score: <b>{player.score}</b>
        {' '}
        <button onClick={handlePlusClick}>
          +1
        </button>
      </label>
      <label>
        First name:
        <input
          value={player.firstName}
          onChange={handleFirstNameChange}
        />
      </label>
      <label>
        Last name:
        <input
          value={player.lastName}
          onChange={handleLastNameChange}
        />
      </label>
    </>
  );
}