icon
2장 : React 기초

state와 useState 훅 기초


지난 장에서 우리는 props를 이용하여 부모 컴포넌트에서 자식 컴포넌트로 데이터를 전달하는 방법을 배웠습니다. props는 컴포넌트 외부에서 전달받는 읽기 전용 데이터였죠. 하지만 웹 애플리케이션은 사용자의 상호작용(버튼 클릭, 입력 등)에 따라 동적으로 UI가 변경되어야 하는 경우가 많습니다.

예를 들어, '좋아요' 버튼을 누르면 좋아요 수가 증가하거나, 입력 필드에 텍스트를 입력하면 해당 텍스트가 화면에 실시간으로 반영되어야 합니다. 이러한 컴포넌트 내부에서 변경될 수 있는 동적인 데이터를 관리하기 위해 리액트는 state(상태) 라는 개념을 제공합니다.

이번 장에서는 리액트의 state가 무엇인지, 왜 중요한지, 그리고 함수형 컴포넌트에서 state를 관리하는 가장 기본적인 방법인 useState 훅(Hook) 에 대해 자세히 알아보겠습니다.


state란 무엇인가?

state는 컴포넌트 내부에서 관리되며, 시간이 지남에 따라 변할 수 있는 데이터를 의미합니다. props가 외부로부터 전달받는 '속성'이라면, state는 컴포넌트 스스로가 '가지고 있는 상태'라고 이해할 수 있습니다. state가 변경되면, 리액트는 해당 state를 사용하는 컴포넌트를 자동으로 재렌더링(re-render) 하여 변경된 UI를 화면에 반영합니다.

state의 주요 특징:

  • 컴포넌트 내부에서 관리: state는 특정 컴포넌트에 종속되며, 그 컴포넌트 내부에서만 변경될 수 있습니다.
  • 가변적인 데이터: 사용자의 상호작용, 네트워크 응답 등 외부 요인에 의해 값이 변경될 수 있습니다.
  • state 변경 시 재렌더링: state가 변경되면 리액트는 자동으로 해당 컴포넌트를 다시 그리고, 변경된 state 값을 반영합니다. 이는 선언적 UI의 핵심 원리 중 하나입니다. 개발자가 직접 DOM을 조작할 필요 없이, state만 변경하면 리액트가 알아서 UI를 업데이트해 줍니다.

useState 훅 소개

과거에는 클래스형 컴포넌트에서만 state를 사용할 수 있었습니다. 하지만 리액트 16.8 버전부터 도입된 Hooks(훅스) 덕분에 함수형 컴포넌트에서도 state를 비롯한 다양한 리액트 기능을 사용할 수 있게 되었습니다. useState는 그중에서도 가장 기본적인 훅이며, 함수형 컴포넌트에서 state를 관리할 수 있도록 해줍니다.

Hooks의 규칙:

  • 최상위에서만 호출: 훅은 리액트 함수 컴포넌트의 최상위(at the top level)에서만 호출해야 합니다. 반복문, 조건문, 중첩된 함수 내에서는 훅을 호출할 수 없습니다.
  • 리액트 함수 컴포넌트에서만 호출: 훅은 일반 자바스크립트 함수에서는 호출할 수 없고, 오직 리액트 함수 컴포넌트 또는 커스텀 훅에서만 호출해야 합니다.

이러한 규칙은 리액트가 훅의 상태를 올바르게 관리할 수 있도록 돕습니다.


useState 훅 사용법 기초

useState 훅은 다음과 같은 형태로 사용합니다.

import React, { useState } from 'react'; // (1) useState 훅 임포트

function MyCounter() {
  // (2) useState 호출: 배열 비구조화 할당으로 state 변수와 setter 함수 얻기
  const [count, setCount] = useState(0); // 초기값은 0

  return (
    <div>
      <p>현재 카운트: {count}</p>
      <button onClick={() => setCount(count + 1)}>증가</button> {/* (3) setter 함수를 이용해 state 변경 */}
    </div>
  );
}

하나씩 자세히 살펴보겠습니다.

  1. useState 임포트: useState 훅을 사용하려면 먼저 react 라이브러리에서 이를 불러와야 합니다.

    import React, { useState } from 'react';
  2. useState 호출 및 반환값: useState 함수를 호출하면, 요소 두 개를 가진 배열을 반환합니다. 우리는 이 배열의 요소를 비구조화 할당(Destructuring Assignment)을 통해 추출하여 사용합니다.

    const [state변수, state변경함수] = useState(초기값);
    • state변수 (예: count): 현재 state의 값을 담고 있는 변수입니다. 이 변수는 state가 변경될 때마다 새로운 값으로 업데이트됩니다.
    • state변경함수 (예: setCount): state의 값을 업데이트하는 함수입니다. 이 함수를 호출할 때 인자로 새로운 state 값을 전달합니다. state는 반드시 이 함수를 통해서만 변경해야 합니다. (직접 state변수 = 새로운값으로 변경하면 UI가 업데이트되지 않습니다.)
    • 초기값 (예: 0): state 변수가 처음 생성될 때 가질 초기값입니다. 문자열, 숫자, 불리언, 객체, 배열 등 어떤 타입이든 올 수 있습니다.

    예시에서 const [count, setCount] = useState(0);count라는 state 변수를 만들고 초기값을 0으로 설정하며, 이 count를 변경할 때는 setCount 함수를 사용하겠다는 의미입니다.

  3. state 변경하기: state를 변경하려면 두 번째 반환값인 state변경함수(예: setCount)를 호출해야 합니다. 이 함수에 새로운 state 값을 인자로 전달합니다.

    <button onClick={() => setCount(count + 1)}>증가</button>

    버튼을 클릭하면 setCount 함수가 호출되고, count의 현재 값에 1을 더한 새로운 값을 count state에 할당합니다. count state가 변경되었으므로, 리액트는 MyCounter 컴포넌트를 다시 렌더링하고 화면의 p 태그에 업데이트된 count 값을 보여줍니다.


실습 예제: 토글 버튼 만들기

useState 훅을 사용하여 간단한 토글(Toggle) 버튼을 만들어 보겠습니다. 버튼을 누를 때마다 텍스트가 '켜짐'과 '꺼짐' 상태로 번갈아 바뀌도록 할 것입니다.

  1. ToggleSwitch.js 컴포넌트 생성: src/components 폴더 안에 ToggleSwitch.js 파일을 생성합니다.

    src/components/ToggleSwitch.js
    // src/components/ToggleSwitch.js
    import React, { useState } from 'react';
    
    function ToggleSwitch() {
      // isToggled: 현재 상태 값 (초기값: false)
      // setIsToggled: isToggled를 변경할 때 사용하는 함수
      const [isToggled, setIsToggled] = useState(false);
    
      const handleToggle = () => {
        // setIsToggled 함수를 사용하여 isToggled의 현재 값을 반전
        setIsToggled(!isToggled);
      };
    
      return (
        <div style={{ padding: '20px', border: '1px solid #ccc', borderRadius: '8px', margin: '20px' }}>
          <h2>토글 스위치</h2>
          <p>
            현재 상태: <span style={{ fontWeight: 'bold', color: isToggled ? 'green' : 'red' }}>
              {isToggled ? '켜짐' : '꺼짐'}
            </span>
          </p>
          <button
            onClick={handleToggle}
            style={{
              padding: '10px 20px',
              fontSize: '16px',
              cursor: 'pointer',
              backgroundColor: isToggled ? '#4CAF50' : '#f44336', // 상태에 따라 버튼 색상 변경
              color: 'white',
              border: 'none',
              borderRadius: '5px'
            }}
          >
            {isToggled ? '끄기' : '켜기'}
          </button>
        </div>
      );
    }
    
    export default ToggleSwitch;
    • useState(false): isToggled라는 state를 선언하고 초기값을 false로 설정했습니다.
    • handleToggle 함수: 버튼 클릭 시 호출될 함수입니다. setIsToggled(!isToggled)를 통해 isToggled의 현재 값을 반전시켜 state를 업데이트합니다.
    • UI 렌더링: isToggled 값에 따라 텍스트 ('켜짐' / '꺼짐')와 버튼의 배경색이 동적으로 변경되도록 했습니다.
  2. App.js에서 ToggleSwitch 컴포넌트 사용: src/App.js 파일을 열고, 기존 내용을 수정하거나 추가하여 ToggleSwitch 컴포넌트를 불러와 사용합니다.

    src/App.js
    // src/App.js
    import React from 'react';
    import './App.css';
    import UserProfile from './components/UserProfile'; // 기존 UserProfile이 있다면 그대로 둡니다.
    import ToggleSwitch from './components/ToggleSwitch'; // ToggleSwitch 컴포넌트 불러오기
    
    function App() {
      // ... 기존 UserProfile 관련 코드 ...
    
      return (
        <div className="App">
          <h1>나의 React 앱</h1>
    
          {/* UserProfile 컴포넌트 (있다면) */}
          {/* <UserProfile name="김리액트" age={28} location="서울" /> */}
    
          {/* ToggleSwitch 컴포넌트 추가 */}
          <ToggleSwitch />
          <ToggleSwitch /> {/* 여러 번 재사용해도 각자 상태를 가집니다! */}
    
        </div>
      );
    }
    
    export default App;
  3. 결과 확인: 브라우저를 확인하면 '토글 스위치'가 렌더링되어 있을 것입니다. 버튼을 클릭할 때마다 텍스트와 버튼의 색상이 바뀌는 것을 확인해 보세요. 특히, ToggleSwitch 컴포넌트를 두 번 사용했지만 각각 독립적으로 상태를 관리하고 있음을 확인할 수 있습니다. 이것이 바로 리액트 state의 강력함입니다!


state 업데이트 시 주의사항

  • 직접 state를 변경하지 마세요! 절대 state변수 = 새로운값과 같이 직접 state 변수에 값을 할당하여 변경해서는 안 됩니다. 이렇게 하면 리액트가 state의 변경을 감지하지 못하여 UI가 재렌더링되지 않습니다.

    // ❌ 잘못된 예시: state를 직접 변경
    const [count, setCount] = useState(0);
    // count = count + 1; // 이렇게 하면 안 됩니다!
    
    // ⭕ 올바른 예시: setter 함수 사용
    setCount(count + 1);
  • 비동기적 업데이트: useStatesetter 함수는 state를 비동기적으로 업데이트할 수 있습니다. 즉, setCount(count + 1)를 호출한 직후에 console.log(count)를 찍으면 아직 이전 값이 나올 수 있습니다. 만약 이전 state 값에 의존하여 새로운 state를 계산해야 한다면, setter 함수에 콜백 함수를 전달하는 것이 안전합니다.

    // 이전 state 값에 기반하여 업데이트할 때
    setCount(prevCount => prevCount + 1);

    prevCountsetCount가 호출될 시점의 최신 count 값을 보장해 줍니다. 이는 특히 여러 번의 state 업데이트가 빠르게 일어날 때 유용합니다. (이 부분은 나중에 더 깊이 다루겠습니다.)


이제 여러분은 propsstate라는 리액트의 양대 산맥 중 하나인 state의 기초를 다진 셈입니다.