Dev Thinking
32완료

상태의 탄생 - useState로 컴포넌트에 기억력 주기

2025-08-12
9분 읽기

리액트 공식문서 기반의 리액트 입문기

들어가며

지난 "3-1편: 클릭에서 이벤트까지 - 리액트 이벤트 처리의 우아함"에서는 우리가 익숙했던 자바스크립트의 addEventListener와 리액트의 이벤트 핸들링 방식의 차이를 살펴보았습니다. 이제 이벤트에 응답하여 무언가를 '기억'하고, 그 기억을 바탕으로 화면을 '변화'시키는 방법에 대해 알아볼 시간입니다.

우리가 만드는 대부분의 애플리케이션은 단순히 정적인 화면을 보여주는 것을 넘어, 사용자의 입력이나 시간의 흐름에 따라 동적으로 변화하는 정보를 다루게 됩니다. 예를 들어, 입력 필드에 텍스트를 입력하면 해당 텍스트가 화면에 표시되고, 그 길이에 따라 특정 UI 요소의 색상이 바뀌는 것과 같은 상호작용들이죠. 이러한 동적인 데이터를 우리는 '상태(State)'라고 부릅니다.

바닐라 자바스크립트로 개발할 때는 이러한 상태를 관리하기 위해 다양한 방법을 사용했습니다. 전역 변수를 피하고자 클로저를 활용하거나, 특정 객체에 상태를 캡슐화하여 관리하기도 했죠. 하지만 UI가 복잡해질수록 상태 관리 코드는 점점 더 복잡해지고, 데이터와 UI의 동기화를 수동으로 처리해야 하는 어려움이 있었습니다.

리액트에서는 useState라는 특별한 Hook을 통해 컴포넌트가 이러한 '기억력'을 가질 수 있도록 돕습니다. 이번 편에서는 우리가 바닐라 자바스크립트로 상태를 관리했던 방식과 useState를 활용한 리액트 방식을 비교하며, 리액트가 어떻게 컴포넌트에 기억력을 부여하고 상태 관리를 더욱 효율적으로 만들어주는지 탐구해보고자 합니다.

리액트 고유 기능 - useState

"자바스크립트엔 없는 리액트의 특징" 중 하나가 바로 useState입니다. useState는 리액트의 함수 컴포넌트 내부에서 상태를 선언하고 관리하게 해주는 특별한 Hook입니다. 이를 통해 함수 컴포넌트가 마치 자신만의 독립적인 메모리를 갖는 것처럼 특정 값을 '기억'할 수 있게 됩니다.

useState를 호출하면 두 가지 요소를 포함하는 배열을 반환합니다.

const [state, setState] = useState(initialState);
  • state: 현재 렌더링을 위한 상태의 값입니다. 이 값은 읽기 전용으로 사용되며, 직접 변경해서는 안 됩니다.
  • setState: 상태를 업데이트하도록 리액트에 '요청'하는 함수(setter 함수)입니다. 이 함수를 호출하면 리액트가 새로운 상태 값으로 컴포넌트를 다시 렌더링하도록 스케줄링합니다. 직접적인 변수 할당이 아니라, setter 함수를 통해 상태를 변경해야만 리액트가 변화를 감지하고 UI를 업데이트합니다.
  • initialState: 상태의 초기값입니다. 컴포넌트가 처음 렌더링될 때 한 번만 사용됩니다.

setter 함수를 호출하는 것은 리액트에게 상태 변경을 알리고, 해당 컴포넌트의 재렌더링을 예약하는 비동기적인 과정입니다. 리액트는 이 과정을 효율적으로 처리하여, 상태와 UI가 항상 동기화되도록 관리합니다. 또한, 각 컴포넌트 인스턴스는 자신만의 독립적인 상태를 가지므로, 동일한 컴포넌트가 여러 번 사용되어도 서로 다른 상태를 유지할 수 있습니다.

잠깐 Q&A

  • 왜 상태를 직접 바꾸면 안 될까요? state = ...처럼 직접 변경하면 리액트가 변화를 감지하지 못해 재렌더링이 일어나지 않습니다. 반드시 setState(여기서는 setText)로 변경을 '요청'해야 합니다.
  • setState 직후 값이 바로 반영되지 않는 이유는? 리액트가 여러 업데이트를 묶어 처리하는 비동기 스케줄링을 하기 때문입니다. 이 배경은 3-4편(스냅샷)과 3-5편(배치/함수형 업데이트)에서 이어서 설명합니다.

자바스크립트로 텍스트 입력 및 동적 배경색 변경 기능 만들기

이제 바닐라 자바스크립트 방식으로 텍스트 입력 필드의 내용을 실시간으로 보여주고, 입력된 텍스트 길이에 따라 배경색이 바뀌는 기능을 만들어보겠습니다. 전역 변수 사용을 지양하고, IIFE(즉시 실행 함수)를 활용하여 스코프를 관리하는 방식으로 작성해보겠습니다.

<!DOCTYPE html>
<html lang="ko">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Vanilla JavaScript Dynamic Input</title>
  </head>
  <body>
    <div id="app">
      <input type="text" id="text-input" placeholder="여기에 텍스트를 입력하세요" />
      <p>입력된 텍스트: <span id="display-text"></span></p>
      <p>텍스트 길이: <span id="text-length">0</span></p>
    </div>
 
    <script>
      (function () {
        let inputText = ""; // 지역 변수로 상태 관리
 
        const appDiv = document.getElementById("app");
        const textInput = document.getElementById("text-input");
        const displayTextSpan = document.getElementById("display-text");
        const textLengthSpan = document.getElementById("text-length");
 
        function updateUI() {
          displayTextSpan.textContent = inputText;
          textLengthSpan.textContent = inputText.length;
 
          if (inputText.length < 5) {
            appDiv.style.backgroundColor = "#ffebee"; // 연한 빨강
          } else if (inputText.length < 10) {
            appDiv.style.backgroundColor = "#fffde7"; // 연한 노랑
          } else {
            appDiv.style.backgroundColor = "#e8f5e9"; // 연한 초록
          }
        }
 
        textInput.addEventListener("input", function (event) {
          inputText = event.target.value; // 상태 변경
          updateUI(); // UI 수동 업데이트
        });
 
        updateUI(); // 초기 화면 렌더링
      })();
    </script>
  </body>
</html>

위 코드는 IIFE를 사용하여 inputText 변수를 지역 스코프에 가두고, input 이벤트 발생 시 inputText 값을 변경한 다음 updateUI 함수를 호출하여 화면의 여러 요소를 수동으로 업데이트합니다. 텍스트 내용(displayTextSpan), 길이(textLengthSpan), 그리고 appDiv의 배경색까지 모두 inputText 상태 변화에 따라 개발자가 직접 조작해야 하는 명령형 방식입니다.

자바스크립트 방식의 특징

  1. 명령형(Imperative) 상태 관리 및 UI 동기화: inputText 변수를 직접 변경하고, updateUI() 함수를 명시적으로 호출하여 UI의 여러 부분을 업데이트해야 합니다. 텍스트 내용, 길이, 배경색 등 모든 변화를 "어떻게(How)" 처리할지 개발자가 세세하게 지시합니다.
  2. 다중 DOM 직접 조작: document.getElementById()를 통해 여러 DOM 요소를 선택하고, textContentstyle.backgroundColor를 사용하여 각 요소를 직접 조작합니다. 하나의 상태 변경이 여러 DOM 조작으로 이어집니다.
  3. 수동적인 의존성 관리: inputText의 변경이 displayTextSpan, textLengthSpan, appDiv의 배경색에 영향을 미친다는 의존성을 개발자가 직접 기억하고 updateUI 함수 내에서 모두 처리해야 합니다. 누락되거나 잘못된 조작은 UI 버그로 이어질 수 있습니다.
  4. 복잡한 조건부 스타일링: 텍스트 길이에 따른 배경색 변경과 같은 조건부 스타일링도 if-else if 문을 사용하여 개발자가 직접 style 속성을 할당해야 합니다. 이는 코드의 가독성을 저해하고 유지보수를 어렵게 만듭니다.
  5. 스코프 관리의 필요성: inputText와 같은 상태 변수가 전역 스코프에 노출되지 않도록 IIFE나 모듈 패턴을 사용하여 명시적으로 스코프를 관리해야 합니다. 애플리케이션의 복잡도가 높아질수록 이러한 관리는 더욱 까다로워집니다.

리액트로 동일한 기능 만들기

이제 리액트 버전으로 넘어가 보겠습니다. 리액트에서는 useState Hook을 사용하여 위와 동일한 기능을 훨씬 간결하고 선언적으로 구현할 수 있습니다. 리액트는 setter 함수를 통한 상태 변경 요청에 따라 컴포넌트 전체를 다시 렌더링하여, 데이터와 UI의 동기화를 자동으로 처리해줍니다.

파일 구조

src/
├── DynamicInput.jsx
└── App.jsx
└── main.jsx (생략)

src/DynamicInput.jsx (동적 입력 컴포넌트)

import { useState } from 'react';
 
function DynamicInput() {
  const [text, setText] = useState(''); // 텍스트 입력 상태
 
  const handleChange = event => {
    setText(event.target.value); // setter 함수를 통한 상태 업데이트 요청
  };
 
  // 텍스트 길이에 따른 배경색 결정
  const getBackgroundColor = () => {
    if (text.length < 5) {
      return '#ffebee'; // 연한 빨강
    } else if (text.length < 10) {
      return '#fffde7'; // 연한 노랑
    } else {
      return '#e8f5e9'; // 연한 초록
    }
  };
 
  return (
    <div style={{ backgroundColor: getBackgroundColor() }}>
      <input
        type="text"
        value={text}
        onChange={handleChange}
        placeholder="여기에 텍스트를 입력하세요"
      />
      <p>
        입력된 텍스트: <span>{text}</span>
      </p>
      <p>
        텍스트 길이: <span>{text.length}</span>
      </p>
    </div>
  );
}
 
export default DynamicInput;

DynamicInput 컴포넌트는 useState Hook을 사용하여 text라는 상태를 관리하며, 이 상태의 변화에 따라 UI를 동적으로 업데이트하는 기능을 구현합니다.

  • const [text, setText] = useState(""): text는 현재 입력 필드의 값(상태)을 저장하고, setText는 이 text 상태를 업데이트하는 함수입니다. 초기값은 빈 문자열("")로 설정됩니다.
  • handleChange 함수: 입력 필드(input)의 onChange 이벤트에 연결되어 있습니다. 사용자가 텍스트를 입력할 때마다 event.target.value를 통해 현재 입력값을 가져와 setText 함수로 text 상태를 업데이트하도록 요청합니다. setText가 호출되면 리액트가 DynamicInput 컴포넌트를 다시 렌더링하도록 스케줄링합니다.
  • getBackgroundColor 함수: 현재 text 상태의 길이에 따라 배경색을 결정하는 헬퍼 함수입니다. 텍스트 길이에 따라 다른 색상을 반환하며, 이는 <div style={{ backgroundColor: getBackgroundColor() }}>와 같이 JSX 내부에서 선언적으로 사용됩니다.

이러한 구조를 통해 개발자는 DOM을 직접 조작하는 대신, text 상태의 변화에 따라 UI가 어떻게 보여야 하는지만을 선언적으로 정의할 수 있습니다. 리액트는 text 상태가 업데이트될 때마다 컴포넌트를 효율적으로 재렌더링하여 화면을 최신 상태로 유지합니다.

src/App.jsx (최상위 앱 컴포넌트)

// App.js (예시 사용)
import DynamicInput from './DynamicInput';
 
function App() {
  return (
    <div className="App">
      <h1>리액트 동적 입력 예제</h1>
      <DynamicInput />
    </div>
  );
}
 
export default App;

App 컴포넌트는 애플리케이션의 최상위 컴포넌트로서, 앞서 구현한 DynamicInput 컴포넌트를 가져와 화면에 렌더링하는 역할을 합니다. 이처럼 리액트에서는 DynamicInput과 같은 독립적인 기능을 가진 컴포넌트들을 조합하여 전체 애플리케이션의 UI를 구성하며, 이는 코드의 모듈성과 재사용성을 높이는 데 크게 기여합니다.

리액트 방식의 특징 (useState)

  • 1. 선언형(Declarative) 상태 관리 및 UI 묘사: useState를 사용하면 개발자는 "이러한 상태(text)를 가질 것이다"라고 선언하는 데 집중합니다. 이는 자바스크립트에서 DOM을 직접 조작하며 "어떻게(How)" UI를 변경할지 명령하는 방식과 대조됩니다. 리액트는 상태(text)가 변경될 때마다 컴포넌트를 다시 렌더링하여 UI의 "모양(What)"을 자동으로 업데이트하므로, 개발자는 상태에 따른 UI의 최종 모습만을 묘사하는 데 집중할 수 있게 됩니다. 이는 코드의 예측 가능성을 높이고, UI 로직을 간결하게 유지하는 리액트의 핵심 철학입니다.
  • 2. Hook(훅)을 통한 상태 캡슐화와 컴포넌트 독립성: useState는 함수 컴포넌트 내에서 상태를 관리할 수 있게 해주는 특별한 함수, 즉 Hook입니다. 이를 통해 text와 같은 상태는 DynamicInput 컴포넌트 내부에 캡슐화되어 외부로부터 격리됩니다. 각 컴포넌트 인스턴스는 자신만의 독립적인 상태를 가지므로, 동일한 컴포넌트가 여러 번 사용되어도 서로 간섭하지 않고 독립적으로 동작합니다. 이는 컴포넌트의 재사용성을 극대화하고, 대규모 애플리케이션에서 상태 관리의 복잡성을 효과적으로 줄여줍니다.
  • 3. setter 함수를 통한 예측 가능한 상태 업데이트: useState에서 반환되는 setter 함수(setText)는 상태를 안전하고 예측 가능하게 업데이트하는 유일한 방법입니다. setter 함수를 호출하면 리액트 내부에서 해당 컴포넌트의 재렌더링이 예약되고, 새로운 상태 값을 기반으로 UI를 효율적으로 업데이트합니다. 직접적인 변수 할당 방식(inputText = event.target.value)에서 발생할 수 있는 데이터 불일치나 사이드 이펙트 문제를 방지하며, 리액트의 최적화된 렌더링 메커니즘을 온전히 활용할 수 있도록 돕습니다. 이 비동기적 특성은 3-4편에서 '스냅샷' 개념으로 더 깊이 다룹니다.
  • 4. 자동적인 UI 동기화와 개발 생산성 향상: setter 함수 호출을 통해 리액트가 상태 변경을 감지하고 해당 컴포넌트를 효율적으로 재렌더링하여 UI를 자동으로 최신 상태로 유지합니다. 텍스트 내용(text), 길이(text.length), 배경색(backgroundColor)이 모두 text 상태에 선언적으로 반응하므로, 개발자가 자바스크립트에서처럼 수동으로 여러 DOM 요소를 동기화할 필요가 없습니다. 이는 개발자가 UI 업데이트 로직에 드는 노력을 최소화하고, 비즈니스 로직 구현에 더 집중할 수 있게 하여 개발 생산성을 크게 향상시킵니다.
  • 5. 가독성 높은 조건부 스타일링과 로직 분리: 텍스트 길이에 따른 배경색 변경과 같은 조건부 스타일링이 JSX 내에서 getBackgroundColor() 함수 호출을 통해 선언적으로 이루어집니다. getBackgroundColor와 같은 헬퍼 함수를 사용하여 스타일 결정 로직을 분리함으로써, 컴포넌트의 렌더링 로직은 깔끔하게 유지되고 코드의 가독성이 높아집니다. 이는 UI와 로직이 명확하게 분리되면서도, JSX 내부에서 동적인 스타일을 유연하게 적용할 수 있는 리액트의 강점을 보여줍니다.

자바스크립트 vs 리액트 차이

이제 두 방식의 주요 차이점을 표로 정리하여 살펴보겠습니다.

구분자바스크립트리액트 (useState)
상태 정의지역 변수, 클로저, 객체 속성 등으로 정의useState Hook을 통해 상태 변수와 설정 함수 반환
상태 변경변수 직접 할당 및 수동 업데이트 함수 호출setter 함수 호출을 통한 리액트 렌더링 요청
UI 업데이트개발자가 DOM을 직접 선택하여 여러 요소를 수동 조작상태 변경 감지 후 리액트가 자동으로 재렌더링
동기화 책임개발자가 상태와 UI 간의 모든 동기화 책임리액트가 setter 함수를 통해 상태와 UI 간의 동기화를 관리

요약

이번 편에서는 애플리케이션의 핵심이라 할 수 있는 '상태'가 무엇인지 살펴보고, 바닐라 자바스크립트와 리액트에서 이 상태를 어떻게 관리하는지 비교해보았습니다. 텍스트 입력에 따라 여러 UI 요소가 동적으로 변화하는 예제를 통해, 우리가 자바스크립트 개발에서 전역 변수 사용을 지양했음에도 불구하고, UI가 복잡해질수록 상태 관리와 여러 UI 요소의 동기화에 많은 노력이 필요하다는 것을 다시금 느꼈으리라 생각합니다.

리액트의 useState Hook은 이러한 문제를 해결하고, 컴포넌트에 '기억력'을 부여하여 동적인 UI를 효율적으로 만들 수 있도록 돕는 강력한 도구입니다.

  • 상태 정의: useState로 간결하게 상태 선언 및 관리
  • 명령형 vs 선언형: useState는 선언형 패러다임으로 UI 자동 동기화
  • setter 함수의 중요성: setter 호출로 리액트 렌더링 및 UI 자동 업데이트

useState를 통해 우리는 컴포넌트가 마치 자신만의 메모리를 가진 것처럼 특정 값을 기억하고 반응하도록 만들 수 있습니다. 이는 우리가 복잡한 상호작용을 가진 UI를 더 쉽고 예측 가능하게 구축할 수 있도록 돕는 리액트의 핵심적인 기능 중 하나입니다. 다음 편에서는 리액트가 상태 변화를 어떻게 감지하고 화면을 업데이트하는지, '렌더링과 커밋' 과정에 대해 알아보겠습니다.

참고문서

State: 컴포넌트의 메모리 (State: A Component's Memory)