본문 바로가기

나야, 리액트 스터디

[week3] - 이벤트에 응답하기, State: 컴포넌트의 기억 저장소, 랜더링 그리고 커밋, 스냅샷으로서의 state

안녕하세요 웹파트 YB 한수정입니다!

벌써 3주차가 밝았습니다. 이번 주는 이벤트에 응답하기, State: 컴포넌트의 기억 저장소, 랜더링 그리고 커밋, 스냅샷으로서의 state에 대해 공부하고 정리해보는 주입니다! 공식 문서를 보며 공부해보니, State는 React에서 매우 중요한 개념으로, 모든 컴포넌트의 동작과 데이터 관리에 깊이 연관되어 있음을 느꼈습니다. 그래서 타이틀에 state가 들어가 있는 State: 컴포넌트의 기억 저장소와 스냅샷으로서의 state에 대한 개념을 한 번 더 정리하려고 합니다!

 

State: 컴포넌트의 기억 저장소

State는 React에서 컴포넌트가 "기억"해야 하는 데이터를 의미합니다. 사용자의 상호작용에 따라 화면 내용을 변경하기 위해 필요한 정보들, 예를 들어 현재 입력값, 현재 보여지는 이미지, 장바구니에 담긴 상품 목록 등이 state로 저장됩니다.

useState 훅으로 state 변수를 추가하는 방법

state는 컴포넌트의 상태를 저장하고, 상태가 변경될 때마다 리액트는 자동으로 컴포넌트를 다시 렌더링하여 사용자 인터페이스(UI)를 최신 상태로 유지합니다.

  1. 지역 변수는 렌더링 간에 유지되지 않습니다. React는 이 컴포넌트를 두 번째로 렌더링할 때 지역 변수에 대한 변경 사항은 고려하지 않고 처음부터 렌더링 합니다.
  2. 지역 변수를 변경해도 렌더링을 일으키지 않습니다. React는 새로운 데이터로 컴포넌트를 다시 렌더링해야 한다는 것을 인식하지 못합니다.

이 두가지 이유로 일반 변수로 충분하지 않은 경우를 설명할 수 있고 useState 훅으로 state 변수를 추가하는 방법에 대해 알아보겠습니다.

import { useState } from 'react';
import { sculptureList } from './data.js';

export default function Gallery() {
  const [index, setIndex] = useState(0);

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

  let sculpture = sculptureList[index];
  return (
    <>
      <button onClick={handleClick}>
        Next
      </button>
      <h2>
        <i>{sculpture.name} </i> 
        by {sculpture.artist}
      </h2>
      <h3>  
        ({index + 1} of {sculptureList.length})
      </h3>
      <img 
        src={sculpture.url} 
        alt={sculpture.alt}
      />
      <p>
        {sculpture.description}
      </p>
    </>
  );
}

data.js에 데이터가 들어있다는 가정 하에 작성된 App.js입니다.63

 

  • state 변수를 추가하기 위해 파일 상단의 React에서 useState를 가져옵니다.
  • const [index, setIndex] = useState(0); 
    useState 훅은 index라는 state변수를 정의하기 위해 사용되었습니다. useState를 사용할 때, 초기값을 괄호 안에 넣어줍니다. index는 현재 상태값을 나타내고, setIndex 함수를 호출할 때마다 컴포넌트는 다시 렌더링 되어 최신 UI가 화면에 나타납니다.
  • handleClick 함수에서 setIndex를 호출하여 index 값을 변경하고 있습니다. 이 버튼을 클릭할 때마다 index가 1씩 증가하여 다음 조각을 표시하게 됩니다.
useState 훅을 사용하면, 상태가 변경될 때마다 리액트가 자동으로 컴포넌트를 다시 렌더링해 UI가 최신 상태로 유지됩니다.

 

 

훅 !

React에서 useState처럼 use로 시작하는 모든 함수를 훅이라고 합니다. 훅은 컴포넌트가 렌더링될 때 특정 React 기능을 연결하는 역할을 합니다.

훅은 반드시 컴포넌트의 최상위 수준에서만 호출해야 하며, 조건문이나 반복문, 중첩 함수 내부에서 호출할 수 없습니다. 

 

useState 훅이 반환하는 한 쌍의 값

React의 useState 훅은 컴포넌트의 상태를 저장하고 관리하기 위한 두 가지 값을 반환합니다.

  • 상태 변수 (예시에서 index)현재 상태의 값을 저장하는 변수로, 컴포넌트가 렌더링될 때마다 이 값이 유지됩니다. useState에 전달한 초기값을 바탕으로 첫 렌더링 시 설정되며, 이후 상태가 갱신되면 자동으로 최신 상태값을 반영하게 됩니다.
  • 상태 갱신 함수 (예시에서 setIndex)상태를 변경할 때 사용하는 함수로, state setter라고도 불립니다. 이 함수를 호출하여 새로운 상태값을 설정하면, React는 해당 컴포넌트를 다시 렌더링하여 UI에 새로운 상태가 반영되도록 합니다.

이와 같은 두 값은 배열 구조로 반환되며, 보통 배열 구조 분해 할당(destructuring)을 사용하여 값을 할당합니다.

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

 

useState훅을 통해 컴포넌트 내부에서 상태를 쉽게 관리할 수 있으며, React는 이 상태가 변경될 때마다 컴포넌트를 자동으로 다시 렌더링해 UI를 최신 상태로 유지할 수 있습니다.

 

 

둘 이상의 state 변수를 추가하는 방법

React에서는 useState 훅을 통해 컴포넌트에 원하는 만큼의 state 변수를 추가할 수 있습니다. 각 useState 호출이 서로 독립적인 상태를 설정하므로, 서로 관련이 없는 값이라면 별도의 state 변수를 사용하는 것이 좋습니다.

 

아래 코드의 컴포넌트에서는 숫자 타입의 index와 불리언 타입의 showMore 두 가지 state 변수를 독립적으로 관리합니다.

import { useState } from 'react';
import { sculptureList } from './data.js';

export default function Gallery() {
  const [index, setIndex] = useState(0);
  const [showMore, setShowMore] = useState(false);

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

  function handleMoreClick() {
    setShowMore(!showMore);
  }

  let sculpture = sculptureList[index];
  return (
    <>
      <button onClick={handleNextClick}>
        Next
      </button>
      <h2>
        <i>{sculpture.name} </i> 
        by {sculpture.artist}
      </h2>
      <h3>  
        ({index + 1} of {sculptureList.length})
      </h3>
      <button onClick={handleMoreClick}>
        {showMore ? 'Hide' : 'Show'} details
      </button>
      {showMore && <p>{sculpture.description}</p>}
      <img 
        src={sculpture.url} 
        alt={sculpture.alt}
      />
    </>
  );
}

 

위 코드에서 index는 이미지 리스트의 현재 인덱스를 나타내고, showMore는 추가 정보를 표시할지에 대한 상태입니다.

이처럼 두 state는 독립적이지만 컴포넌트의 상태 관리에 있어 중요한 역할을 합니다.

 

 

여러 state 변수를 사용할 때의 고려 사항

만약 두 상태를 자주 함께 변경하거나 연관성이 높다면 하나의 state로 관리하는 것이 좋을 수 있습니다. 예를 들어, 필드가 많은 폼에서는 각 필드마다 개별적인 state를 사용하는 것보다 객체 형태의 하나의 state를 사용하면 코드가 간결해지고 관리가 쉬워집니다.

예시로, 아래의 MovingDot 예시처럼 x와 y 좌표가 함께 업데이트되어야 하는 경우, 개별 state로 나누기보다 position이라는 하나의 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를 지역적이라고 하는 이유

state가 지역적이라고 하는 이유는 컴포넌트 내에서만 유효하기 때문입니다. 같은 컴포넌트를 여러 번 렌더링하더라도, 각 인스턴스는 자신만의 state를 독립적으로 유지합니다.

import Gallery from './Gallery.js';

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

 

예를 들어 <Gallery /> 컴포넌트를 두 번 렌더링하면, 각 갤러리는 버튼 클릭 시 독립적으로 동작하여 서로의 state에 영향을 주지 않습니다. 이는 state가 특정 함수 호출이나 위치에 제한되지 않고 화면의 특정 위치에 지역적으로 연결되어 있기 때문입니다. 이를 통해 각 컴포넌트는 독립적으로 동작하고, 서로 영향을 미치지 않도록 할 수 있습니다.

비공개성과 격리성 덕분에 우리는 컴포넌트 내부에서만 state를 안전하게 다룰 수 있고, 다른 컴포넌트의 상태와 혼동 없이 독립적으로 관리할 수 있습니다.

 

스냅샷으로서의 State

state는 스냅샷처럼 동작합니다. state 변수를 설정하여도 이미 가지고 있는 state 변수는 변경되지 않고, 대신 리렌더링이 발동됩니다.

state 설정으로 리렌더링이 동작하는 방식

React에서 렌더링이란, 컴포넌트를 다시 호출하여 그 시점의 UI 상태를 계산하고 화면에 표시하는 과정을 의미합니다. 이때 상태(state)는 UI의 스냅샷을 찍는 중요한 역할을 합니다.

 

렌더링은 그 시점의 스냅샷을 찍습니다. React는 렌더링을 수행할 때 컴포넌트 함수를 호출하고, 그 함수 내에서 props, 이벤트 핸들러, 로컬 변수와 같은 값들을 현재 state를 사용하여 계산합니다. 상태(state)는 UI를 구성하는 중요한 데이터로, UI는 동적으로 변화하기 때문에 매 렌더링마다 state는 해당 시점의 UI 상태를 반영하는 스냅샷처럼 동작합니다.

 

렌더링의 과정은 아래 그림과 같습니다.

  • React가 함수를 호출합니다: 컴포넌트 함수가 다시 실행되어 UI가 계산됩니다.
  • 스냅샷을 계산합니다: 함수 내에서 state와 props를 이용해 새로운 UI가 계산됩니다.
  • DOM tree를 업데이트합니다: 계산된 UI에 따라 화면이 업데이트되고, DOM이 변경됩니다.

 

state 업데이트 시기 및 방법

state는 컴포넌트 외부에서 React가 관리합니다. 컴포넌트 함수가 호출된 후에도 state 값은 React에 의해 계속 유지되며, 해당 컴포넌트가 다시 렌더링될 때마다 현재 상태값을 제공하는 역할을 합니다. 이를 통해 React는 각 렌더링에서 현재 상태에 맞는 UI를 그릴 수 있습니다.

 

React에서 상태 업데이트 과정은 아래 그림과 같습니다.

  1. React에 state 업데이트를 지시합니다: 상태 변경을 요청하는 setState 함수(예: setNumber가 호출됩니다).
  2. React가 상태 값을 업데이트합니다: setState는 상태 값을 실제로 변경하고, 변경된 상태에 맞춰 다음 렌더링을 준비합니다.
  3. 상태값의 스냅샷을 컴포넌트에 전달합니다: React는 업데이트된 상태를 컴포넌트에 전달하고, 컴포넌트는 이 상태를 사용하여 UI를 다시 렌더링합니다.

상태는 항상 최신 상태를 반영하며, 변경된 값을 다음 렌더링에서 사용합니다.

 

state를 업데이트하는 setState 함수는 비동기적으로 작동해 setState를 호출해도 바로 값이 반영되지 않으며, React는 이를 다음 렌더링 주기에 반영합니다.

 

버튼을 클릭하면 setNumber(number + 1)이 세 번 호출되지만, number는 1로만 업데이트됩니다. 그 이유는 setState가 비동기적으로 작동하기 때문입니다. 각각의 setNumber 호출은 상태 업데이트가 비동기적으로 처리되는 특성상, 호출 당시의 number 값을 참조합니다. 즉, 세 번의 호출 모두 최초 상태인 0을 기준으로 업데이트되기 때문에, 최종적으로 number는 1로만 변경됩니다.

import { useState } from 'react';

export default function Counter() {
  const [number, setNumber] = useState(0);

  return (
    <>
      <h1>{number}</h1>
      <button onClick={() => {
        setNumber(number + 1);
        setNumber(number + 1);
        setNumber(number + 1);
      }}>+3</button>
    </>
  );
}

 

추가적으로, 여러 번 상태를 업데이트해야 할 경우, 상태를 이전 값에 기반하여 업데이트하려면 함수형 업데이트를 사용하는 것이 좋습니다. 아래는 예시 코드입니다.

setNumber(prevNumber => prevNumber + 1);
setNumber(prevNumber => prevNumber + 1);
setNumber(prevNumber => prevNumber + 1);

 

state를 설정한 직후에 state가 업데이트되지 않는 이유

state 업데이트가 즉시 반영되지 않는 이유는 setState 함수가 비동기적으로 동작하기 때문입니다.

React에서 setState가 호출되면, 상태 변경이 즉시 반영되는 것이 아니라, React가 상태 변경을 큐에 쌓고, 다음 렌더링 주기에서 상태 값을 업데이트합니다. 즉, setState가 호출되면 React는 변경될 상태를 기록하고, 현재 실행 중인 코드에서는 그 값을 즉시 반영하지 않습니다.

 

아래 코드에서는 setNumber(number + 5)가 호출된 후 alert(number)를 실행해, 경고창에 0이 표시됩니다. setNumber가 비동기적으로 처리되기 때문에, setNumber(number + 5)가 실행될 때의 number는 여전히 0이기 때문입니다. 

import { useState } from 'react';

export default function Counter() {
  const [number, setNumber] = useState(0);

  return (
    <div>
      <h1>{number}</h1>
      <button onClick={() => {
        setNumber(number + 5);
        alert(number);
      }}>+5</button>
    </div>
  );
}

setState가 호출된 직후에 상태 값이 즉시 업데이트되지 않는 것은 React의 비동기적 상태 업데이트 때문이며, 이를 통해 성능 최적화와 불필요한 렌더링을 방지할 수 있습니다. 

 

이벤트 핸들러가 state의 “스냅샷”에 접근하는 방법

  • 이벤트 핸들러와 State 스냅샷React에서 이벤트 핸들러 내에서 state를 참조하면, 그 값은 현재 렌더링 시점의 값입니다. React는 이벤트가 발생할 때마다 해당 시점의 state 값을 스냅샷처럼 고정하여, 이벤트 핸들러에서 그 값을 사용하게 됩니다. 이벤트 핸들러 내에서 state가 변경되더라도, 해당 렌더링에서 사용하는 state 값은 변하지 않습니다.
import { useState } from 'react';

export default function Form() {
  const [to, setTo] = useState('Alice');
  const [message, setMessage] = useState('Hello');

  function handleSubmit(e) {
    e.preventDefault();
    setTimeout(() => {
      alert(`You said ${message} to ${to}`);
    }, 5000);
  }

  return (
    <form onSubmit={handleSubmit}>
      <label>
        To:{' '}
        <select
          value={to}
          onChange={e => setTo(e.target.value)}>
          <option value="Alice">Alice</option>
          <option value="Bob">Bob</option>
        </select>
      </label>
      <textarea
        placeholder="Message"
        value={message}
        onChange={e => setMessage(e.target.value)}
      />
      <button type="submit">Send</button>
    </form>
  );
}
  • 코드 동작 설명: 
    1. 처음 To 필드는 "Alice"로 설정되어 있습니다.
    2. 사용자가 메시지를 작성하고, "Send" 버튼을 클릭합니다.
    3. 클릭 후 5초가 지난 후 setTimeout으로 alert가 호출됩니다.
    4. 그 사이에 To 값을 "Bob"으로 변경했다면, alert에 표시되는 내용은 무엇인가요?
  • 결과:
    • 예상 결과: alert에는 "You said Hello to Alice"가 표시됩니다.
    • 왜? 이벤트 핸들러는 렌더링 시점의 state 값을 스냅샷으로 고정합니다. 즉, to의 값은 클릭 시점에 "Alice"로 고정되고, 5초 후에 alert가 실행될 때에도 이 값은 변경되지 않은 채 그대로 유지됩니다.

 

여러 state 변수를 사용할 때의 고려 사항 부분에서 useState() 안에 x, y처럼 여러 값이 들어가는 경우를 처음 봐서 새로운 예제 코드가 있는지 궁금합니다. 또한, 스터디원분들이 사용 경험이 있다면, 언제 사용해 보셨는지 궁금합니당

 

이번 주도 읽어주셔서 감사합니다~! 나리스 파이팅