흰 스타렉스에서 내가 내리지

이벤트에 응답하기 - 발생할 수 있는 버그 본문

React

이벤트에 응답하기 - 발생할 수 있는 버그

주씨. 2024. 5. 19. 00:14
728x90

스냅샷으로서의 상태(state)

일반적인 JavaScript 변수와 달리 React 상태(state)는 스냅샷과 유사하게 작동합니다.

상태를 설정하면 이미 있는 상태 변수가 변경되지 않고 다시 렌더링이 트리거됩니다.

 

console.log(count); // 0
setCount(count + 1); // 1로 다시 렌더링을 요청
console.log(count); // 여전히 0!

 

 

 

여러 개의 상태 업데이트를 큐에 저장하기

이 컴포넌트는 버그가 있습니다. "더하기 3"을 클릭하면 점수가 한 번만 증가합니다.

import { useState } from 'react';

export default function Counter() {
  const [score, setScore] = useState(0);

  function increment() {
    setScore(score + 1);
  }

  return (
    <>
      <button onClick={() => increment()}>+1</button>
      <button onClick={() => {
        increment();
        increment();
        increment();
      }}>+3</button>
      <h1>Score: {score}</h1>
    </>
  )
}

상태는 스냅샷으로서에서 이런 현상이 발생하는 이유에 대해 설명합니다.

상태를 설정하면 새로운 렌더링을 요청하지만 이미 실행 중인 코드에서는 변경되지 않습니다.

따라서 setScore(score + 1)를 호출한 후에도 점수는 여전히 0으로 유지됩니다.

 

이를 해결하기 위해 상태를 설정할 때 업데이트 함수를 전달할 수 있습니다. 

setScore(score + 1) setScore(s => s + 1)로 바꾸면 "더하기 3" 버튼이 제대로 작동합니다.

이를 통해 여러 상태 업데이트를 큐에 저장할 수 있습니다.

 

import { useState } from 'react';

export default function Counter() {
  const [score, setScore] = useState(0);

  function increment() {
    setScore(s => s + 1);
  }

  return (
    <>
      <button onClick={() => increment()}>+1</button>
      <button onClick={() => {
        increment();
        increment();
        increment();
      }}>+3</button>
      <h1>Score: {score}</h1>
    </>
  )
}

 

 

상태에서 객체 업데이트하기

상태는 객체를 포함한 모든 종류의 JavaScript 값으로 유지할 수 있습니다.

그러나 React 상태에서 직접 객체와 배열을 변경해서는 안 됩니다.

객체와 배열을 업데이트하려면 새로운 객체를 생성하거나(또는 기존 객체의 사본을 만들거나) 상태를 새로운 객체를 사용하도록 업데이트해야 합니다.

 

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

 

코드에서 객체를 복사하는 작업이 지루하다면, 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;
    });
  }

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

 

 

상태에서 배열 업데이트하기

배열은 상태에 저장할 수 있는 또 다른 유형의 변경 가능한 JavaScript 객체이며 읽기 전용으로 처리해야 합니다.

객체와 마찬가지로 상태에 저장된 배열을 업데이트하려면 새로운 배열을 생성하거나(또는 기존 배열의 사본을 만들거나) 상태를 새 배열을 사용하도록 설정해야 합니다.

 

import { useState } from 'react';

let nextId = 3;
const initialList = [
  { id: 0, title: 'Big Bellies', seen: false },
  { id: 1, title: 'Lunar Landscape', seen: false },
  { id: 2, title: 'Terracotta Army', seen: true },
];

export default function BucketList() {
  const [list, setList] = useState(
    initialList
  );

  function handleToggle(artworkId, nextSeen) {
    setList(list.map(artwork => {
      if (artwork.id === artworkId) {
        return { ...artwork, seen: nextSeen };
      } else {
        return artwork;
      }
    }));
  }

  return (
    <>
      <h1>Art Bucket List</h1>
      <h2>My list of art to see:</h2>
      <ItemList
        artworks={list}
        onToggle={handleToggle} />
    </>
  );
}

function ItemList({ artworks, onToggle }) {
  return (
    <ul>
      {artworks.map(artwork => (
        <li key={artwork.id}>
          <label>
            <input
              type="checkbox"
              checked={artwork.seen}
              onChange={e => {
                onToggle(
                  artwork.id,
                  e.target.checked
                );
              }}
            />
            {artwork.title}
          </label>
        </li>
      ))}
    </ul>
  );
}

 

코드에서 배열을 복사하는 작업이 지루하다면, Immer와 같은 라이브러리를 사용하여 반복적인 코드를 줄일 수 있습니다.

 

import { useState } from 'react';
import { useImmer } from 'use-immer';

let nextId = 3;
const initialList = [
  { id: 0, title: 'Big Bellies', seen: false },
  { id: 1, title: 'Lunar Landscape', seen: false },
  { id: 2, title: 'Terracotta Army', seen: true },
];

export default function BucketList() {
  const [list, updateList] = useImmer(initialList);

  function handleToggle(artworkId, nextSeen) {
    updateList(draft => {
      const artwork = draft.find(a =>
        a.id === artworkId
      );
      artwork.seen = nextSeen;
    });
  }

  return (
    <>
      <h1>Art Bucket List</h1>
      <h2>My list of art to see:</h2>
      <ItemList
        artworks={list}
        onToggle={handleToggle} />
    </>
  );
}

function ItemList({ artworks, onToggle }) {
  return (
    <ul>
      {artworks.map(artwork => (
        <li key={artwork.id}>
          <label>
            <input
              type="checkbox"
              checked={artwork.seen}
              onChange={e => {
                onToggle(
                  artwork.id,
                  e.target.checked
                );
              }}
            />
            {artwork.title}
          </label>
        </li>
      ))}
    </ul>
  );
}