React State 정리

Yun·2024년 6월 28일
0

개인공부

목록 보기
21/28

React 공식문서를 번역 정리한 내용입니다.

State: A Component's Memory

컴포넌트는 상호작용의 결과로 화면에 표시되는 내용을 변경해야 한다. 폼에 입력하면 입력 필드가 업데이트되어야 하고, 다음 이미지로 넘어가는 버튼을 클릭하면 표시되는 이미지가 변경되어야 하며, 구매 버튼을 클릭하면 장바구니에 제품이 담겨야 한다. 컴포넌트는 현재 입력값, 현재 이미지, 장바구니 같은 것들을 "기억"해야 한다. React는 이와 같은 컴포넌트 특유의 메모리를 state라고 부른다.

지역 변수의 한계

아래 코드는 Next를 클릭하면 index가 변경되도록 설계되었지만, 작동하지 않는다.

export default function Gallery() {
  let index = 0;

  function handleClick() {
    index = index + 1;
  }

  let sculpture = sculptureList[index];
  return (
    <>
      <button onClick={handleClick}>
        Next
      </button>
      ({index + 1} of {sculptureList.length})
      // 예시: (1 of 12)
    </>
  );
}

handleClick 이벤트 핸들러가 지역 변수 index를 업데이트한다. 하지만 다음과 같은 이유 때문에 변경 사항이 화면에 반영되지 않는다.

  • 지역 변수는 렌더링 사이에 유지되지 않는다. React가 이 컴포넌트를 두 번째로 렌더링할 때, 지역 변수의 변경 사항을 기억하지 않고 초기화한다.

  • 지역 변수의 변경은 렌더링을 발생시키지 않는다. React는 새로운 데이터 때문에 컴포넌트를 다시 렌더링해야 할 필요가 있다는 것을 인식하지 못한다.

useState 분석

const [index, setIndex] = useState(0);

index는 state 변수이고, setIndex는 설정자 함수이다.

useState의 유일한 인수는 state 변수의 초기값으로, 예제에서는 0으로 설정되어 있다.

컴포넌트가 렌더링될 때마다 useState는 두 개의 값을 포함하는 배열을 반환한다.

  • 저장된 값을 가진 상태 변수 index
  • 상태 변수를 업데이트하고 React가 컴포넌트를 다시 렌더링하도록 트리거할 수 있는 상태 변경 함수 setIndex

아래는 변수를 업데이트하는 예제이다.

function handleClick() {
  setIndex(index + 1);
}

실제 동작 과정

  1. 컴포넌트가 처음 렌더링된다. 0을 초기값으로 설정했기 때문에, [0, setIndex]가 반환된다. React는 index의 최신 상태 값이 0이라는 것을 기억한다.

  2. 사용자가 버튼을 클릭하면 setIndex(index + 1)가 호출된다. 이 경우엔 Index(1)이 호출될 것이다. 이는 React에게 index가 이제 1이라는 것을 알려주고, 렌더링을 트리거한다.

  3. React는 여전히 useState(0)을 보지만, index1로 설정되었다는 것을 기억하므로 [1, setIndex]를 반환한다.

  4. 위 과정이 반복된다.

격리된 State

State는 화면에 있는 컴포넌트 인스턴트에 국한된다. 다시 말해, 동일한 컴포넌트를 두 번 렌더링하면 각 사본은 완전히 독립된 상태를 가진다. 그 중 하나를 변경해도 다른 것에는 영향을 미치지 않는다.

import Gallery from './Gallery.js';

export default function Page() {
  return (
    <div className="Page">
      <Gallery />
      <Gallery />
    </div>
  );
}

위 예시는 동일한 <Gallery /> 컴포넌트를 두 번 렌더링하지만, 각각 독립적인 상태를 가진다.

State는 특정 함수 호출에 묶이지 않고, 코드의 특정 위치에 묶이지도 않지만, 화면의 특정 위치에 지역적(local)이다. 두 개의 컴포넌트를 렌더링했으므로 state는 별도로 저장된다.

Page 컴포넌트는 Gallery의 상태나 상태의 존재 여부도 알지 못한다. props와 달리 state는 선언된 컴포넌트 외엔 완전한 비공개이며, 부모 컴포넌트는 이를 변경할 수 없다. 따라서 다른 컴포넌트에 영향 없이 state를 마음껏 추가하고 제거할 수 있다.

Gallery의 state를 동기화하는 방법은 자식 컴포넌트에서 state를 제거하고 가장 가까운 공유 부모 컴포넌트에 추가하는 것이다.


Choosing the State Structure

State를 잘 구조화하면 수정과 디버깅이 편한 컴포넌트를 만들 수 있다.

구조화 원칙

어떤 state를 보유하는 컴포넌트를 작성할 때, 얼마나 많은 상태 변수를 사용할지, 그리고 그 데이터를 어떤 형태로 할지에 대해 선택해야 한다. 아래는 더 나은 선택을 위한 몇 가지 원칙이다.

  • 관련 state를 그룹화한다. 두 개 이상의 상태 변수를 항상 동시에 업데이트한다면, 그것들을 하나로 합치는 것을 고려하자.

  • state 모순을 피한다. 여러 state 조각이 서로 모순되고 동의하지 않는 방식으로 구조화된다면 실수할 여지가 있으니 피하자.

  • 불필요한 state을 피한다. 렌더링 중에 컴포넌트의 props나 기존 state 변수에서 정보를 계산할 수 있다면 해당 정보를 컴포넌트의 state에 넣지 말자.

  • 중복된 state를 피한다. 여러 state 변수 간에, 혹은 중첩된 객체 내에 동일한 데이터가 중복되면 동기화하기 어렵다.

  • 깊이 중첩된 state를 피한다. 깊게 계층화된 state는 업데이트하기 힘들다. 가능하다면 평평하게 구조화하자.

원칙의 목표는 실수를 유발하지 않고 state를 쉽게 업데이트할 수 있도록 하는 것이다. 중복되거나 반복되는 데이터를 제거하면 모든 조각이 동기화된 state를 유지하는데 도움이 된다.

state 그룹화

단일 state 변수와 다중 state 변수가 있다.

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

두 가지 접근 방식 중 하나를 사용할 수 있지만, 두 개 이상의 state 변수가 항상 함께 변경되는 경우엔 하나의 state 변수로 통합하는 것이 좋다.

데이터를 그룹화하는 또 다른 경우는 필요한 state 조각 수를 모르는 경우이다. 예를 들어, 사용자가 사용자 정의 필드를 추가할 수 있는 양식이 있는 경우 유용하다.

⚠️ State 변수가 객체인 경우 하나의 필드만 업데이트할 수 없다는 점을 기억하자!
예를 들어 x와 y 좌표를 함께 다루는 경우, setPosition({ x: 100 }는 불가능하다. 하나만 설정하고 싶은 경우엔 setPosition({ ...position, x: 100 })처럼 하거나 두 가지 state 변수로 분할해야 한다.

state 모순 피하기

  const [text, setText] = useState('');
  const [isSending, setIsSending] = useState(false);
  const [isSent, setIsSent] = useState(false);

  async function handleSubmit(e) {
    e.preventDefault();
    setIsSending(true);
    await sendMessage(text);
    setIsSending(false);
    setIsSent(true);
  }

위 코드는 작동하지만, 불가능한 state를 허용한다. isSendingisSent가 동시에 true인 상황이 발생할 위험이 있다. 컴포넌트가 복잡할수록 무슨 일이 일어났는지 이해하기 어려울 것이다.

  const [text, setText] = useState('');
  const [status, setStatus] = useState('typing');

  async function handleSubmit(e) {
    e.preventDefault();
    setStatus('sending');
    await sendMessage(text);
    setStatus('sent');
  }

실수가 발생하지 않도록 이처럼 하나의 state 변수로 바꾸어 관리하는 것이 좋다.

불필요한 state 피하기

렌더링하는 동안 컴포넌트의 props나 기존 state 변수에서 일부 정보를 계산할 수 있다면, 해당 정보를 컴포넌트 state에 넣지 않아야 한다.

  const [firstName, setFirstName] = useState('');
  const [lastName, setLastName] = useState('');
  const [fullName, setFullName] = useState('');

  function handleFirstNameChange(e) {
    setFirstName(e.target.value);
    setFullName(e.target.value + ' ' + lastName);
  }

  function handleLastNameChange(e) {
    setLastName(e.target.value);
    setFullName(firstName + ' ' + e.target.value);
  }

firstNamelastName 만으로 fullName을 충분히 계산할 수 있으므로, 아래처럼 바꾸면 좋다.

  const [firstName, setFirstName] = useState('');
  const [lastName, setLastName] = useState('');

  const fullName = firstName + ' ' + lastName;

  function handleFirstNameChange(e) {
    setFirstName(e.target.value);
  }

  function handleLastNameChange(e) {
    setLastName(e.target.value);
  }

업데이트를 위한 별도의 작업은 필요하지 않다. setFirstNamesetLastName가 호출되면 렌더링이 일어나 fullName이 새롭게 계산된다.

props를 state에 그대로 미러링하면 안 된다

function Message({ messageColor }) {
  const [color, setColor] = useState(messageColor);

부모 컴포넌트가 나중에 다른 messageColor를 전달하더라도 color는 업데이트되지 않는다. state 는 첫 렌더링에서만 초기화되기 때문이다.

중복된 state 피하기

아래 코드에서 selectedItemitems의 첫 번째 항목과 동일하다.

  const [items, setItems] = useState(['A', 'B', ..., 'Z']);
  const [selectedItem, setSelectedItem] = useState(
    items[0]
  );

하지만 items의 의 첫 번째 항목을 AA로 업데이트 하더라도 selectedItem은 여전히 A를 저장한다. 각 상태는 독립적으로 관리되기 때문에, selectedItem를 업데이트하지 않으면 변경사항이 반영되지 않는다.

  const [items, setItems] = useState(initialItems);
  const [selectedId, setSelectedId] = useState(0);

중복된 내용을 제거하고 다른 방식으로 접근하는 것이 더 좋다.

깊이 중첩된 state 피하기

const [plan, setPlan] = useState(initialTravelPlan);

// initialTravelPlan
export const initialTravelPlan = {
  id: 0,
  title: '(Root)',
  childPlaces: [{
    id: 1,
    title: 'Earth',
    childPlaces: [{
      id: 2,
      title: 'Africa',
      childPlaces: [{
        id: 3,
        title: 'Botswana',
        childPlaces: []
      }, {
        id: 4,
        title: 'Egypt',
        childPlaces: []
      }, { 
        ...

장소를 삭제하는 버튼을 추가한다고 가정해 보자. 중첩된 state를 업데이트할 땐 변경된 부분부터 객체의 복사본을 만들게 된다. 깊게 중첩된 장소를 삭제하려면 상위 장소 체인 전체를 복사해야만 한다.

state가 너무 중첩되면 업데이트가 어려워진다. 따라서 구조를 평평하게 만드는 것이 좋다. 이는 flat 또는 normalized라고 한다.

export const initialTravelPlan = {
  0: {
    id: 0,
    title: '(Root)',
    childIds: [1, 42, 46],
  },
  1: {
    id: 1,
    title: 'Earth',
    childIds: [2, 10, 19, 26, 34]
  },
  2: {
    id: 2,
    title: 'Africa',
    childIds: [3, 4, 5, 6 , 7, 8, 9]
  }, 
  ...

0개의 댓글