React 독학 가이드 7 - State와 Props의 관계

learning by Seven Fingers Studio 17분
ReactStateProps데이터흐름컴포넌트통신

Props랑 State를 따로 배웠는데, 둘이 같이 쓰면 진짜 강력해져요. 컴포넌트끼리 데이터를 주고받으면서 복잡한 앱도 만들 수 있거든요.

Props vs State 비교

먼저 둘의 차이를 확실히 정리해볼게요.

PropsState
누가 관리?부모 컴포넌트해당 컴포넌트
변경 가능?읽기 전용 (변경 불가)변경 가능
용도외부에서 받는 데이터내부에서 관리하는 데이터
변경 시부모가 새 값을 전달해야 함setState로 변경

쉽게 말해서:

  • Props: 부모가 자식에게 주는 선물 (못 바꿈)
  • State: 내 방에 있는 내 물건 (바꿀 수 있음)

부모의 State를 자식에게 Props로 전달

가장 기본적인 패턴이에요. 부모가 State를 가지고, 자식에게 Props로 넘겨주는 거죠.

// 부모 컴포넌트
function Parent() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <h1>부모 컴포넌트</h1>
      <button onClick={() => setCount(count + 1)}>증가</button>

      {/* State를 Props로 전달 */}
      <Child count={count} />
    </div>
  );
}

// 자식 컴포넌트
function Child({ count }) {
  return (
    <div>
      <h2>자식 컴포넌트</h2>
      <p>부모에서 받은 count: {count}</p>
    </div>
  );
}

실행 결과:

부모 컴포넌트

현재 count: 5

자식 컴포넌트

부모에서 받은 count: 5

부모의 State가 Props로 자식에게 전달되어 동기화됩니다

부모에서 버튼 누르면 count가 바뀌고, 자식도 자동으로 업데이트돼요!

자식이 부모의 State를 바꾸고 싶을 때

자식은 Props를 직접 못 바꿔요. 근데 부모의 State를 바꾸고 싶으면 어떡하죠?

해결책: 부모가 State 변경 함수를 Props로 넘겨주면 돼요!

// 부모 컴포넌트
function Parent() {
  const [count, setCount] = useState(0);

  const handleIncrease = () => {
    setCount(count + 1);
  };

  return (
    <div>
      <h1>카운트: {count}</h1>
      {/* 함수도 Props로 전달 가능! */}
      <Child onIncrease={handleIncrease} />
    </div>
  );
}

// 자식 컴포넌트
function Child({ onIncrease }) {
  return (
    <button onClick={onIncrease}>
      자식에서 증가시키기
    </button>
  );
}

자식에서 버튼을 눌러도 부모의 State가 바뀌어요!

State 끌어올리기 (Lifting State Up)

형제 컴포넌트끼리 데이터를 공유하고 싶으면 어떡할까요?

React에서는 형제끼리 직접 통신이 안 돼요. 그래서 공통 부모로 State를 끌어올려야 해요.

문제 상황

// ❌ 이렇게 하면 A와 B가 서로의 데이터를 모름
function ComponentA() {
  const [text, setText] = useState("");
  return <input value={text} onChange={e => setText(e.target.value)} />;
}function ComponentB() {
  // ComponentA의 text를 어떻게 알지?
  return <p>입력값: ???</p>;
}

해결: State를 부모로 끌어올리기

// ⭕ 부모가 State를 관리
function Parent() {
  const [text, setText] = useState("");

  return (
    <div>
      <InputComponent text={text} onTextChange={setText} />
      <DisplayComponent text={text} />
    </div>
  );
}

function InputComponent({ text, onTextChange }) {
  return (
    <input
      value={text}
      onChange={(e) => onTextChange(e.target.value)}
      placeholder="입력하세요"
    />
  );
}

function DisplayComponent({ text }) {
  return <p>입력값: {text}</p>;
}

이제 InputComponent에서 입력하면 DisplayComponent에도 바로 보여요!

실전 예제: 온도 변환기

섭씨와 화씨를 동시에 보여주는 앱을 만들어볼게요.

import { useState } from 'react';

function TemperatureConverter() {
  const [celsius, setCelsius] = useState("");

  const handleCelsiusChange = (value) => {
    setCelsius(value);
  };

  const handleFahrenheitChange = (value) => {
    // 화씨를 섭씨로 변환해서 저장
    const c = ((value - 32) * 5) / 9;
    setCelsius(value ? c.toFixed(2) : "");
  };

  // 섭씨를 화씨로 변환
  const fahrenheit = celsius ? ((celsius * 9) / 5 + 32).toFixed(2) : "";

  return (
    <div>
      <h1>온도 변환기</h1>
      <TemperatureInput
        scale="섭씨"
        value={celsius}
        onChange={handleCelsiusChange}
      />
      <TemperatureInput
        scale="화씨"
        value={fahrenheit}
        onChange={handleFahrenheitChange}
      />
    </div>
  );
}

function TemperatureInput({ scale, value, onChange }) {
  return (
    <div>
      <label>{scale}: </label>


      <input
        type="number"
        value={value}
        onChange={(e) => onChange(e.target.value)}
      />
    </div>
  );
}

섭씨 입력하면 화씨가 자동 계산되고, 화씨 입력하면 섭씨가 자동 계산돼요!

실전 예제: 탭 컴포넌트

import { useState } from 'react';

function TabContainer() {
  const [activeTab, setActiveTab] = useState(0);

  const tabs = [
    { title: "홈", content: "홈 페이지 내용입니다." },
    { title: "소개", content: "소개 페이지 내용입니다." },
    { title: "연락처", content: "연락처 페이지 내용입니다." }
  ];

  return (
    <div>
      <TabButtons
        tabs={tabs}
        activeTab={activeTab}
        onTabChange={setActiveTab}
      />
      <TabContent content={tabs[activeTab].content} />
    </div>
  );
}

function TabButtons({ tabs, activeTab, onTabChange }) {
  return (
    <div style={{ display: 'flex', gap: '10px', marginBottom: '20px' }}>
      {tabs.map((tab, index) => (
        <button
          key={index}
          onClick={() => onTabChange(index)}
          style={{
            padding: '10px 20px',
            backgroundColor: activeTab === index ? '#007bff' : '#e0e0e0',
            color: activeTab === index ? 'white' : 'black',
            border: 'none',
            borderRadius: '5px',
            cursor: 'pointer'
          }}
        >
          {tab.title}
        </button>
      ))}
    </div>
  );
}

function TabContent({ content }) {
  return (
    <div style={{
      padding: '20px',
      border: '1px solid #ddd',
      borderRadius: '5px'
    }}>
      {content}
    </div>
  );
}

실전 예제: 폼과 미리보기

import { useState } from 'react';

function FormWithPreview() {
  const [formData, setFormData] = useState({
    title: "",
    content: "",
    author: ""
  });

  const handleChange = (field, value) => {
    setFormData({ ...formData, [field]: value });
  };

  return (
    <div style={{ display: 'flex', gap: '40px' }}>
      <FormSection formData={formData} onChange={handleChange} />
      <PreviewSection formData={formData} />
    </div>
  );
}

function FormSection({ formData, onChange }) {
  return (
    <div style={{ flex: 1 }}>
      <h2>작성</h2>
      <div style={{ marginBottom: '15px' }}>
        <label>제목:</label>
        <input
          value={formData.title}
          onChange={(e) => onChange('title', e.target.value)}
          style={{ width: '100%', padding: '8px' }}
        />
      </div>
      <div style={{ marginBottom: '15px' }}>
        <label>작성자:</label>
        <input
          value={formData.author}
          onChange={(e) => onChange('author', e.target.value)}
          style={{ width: '100%', padding: '8px' }}
        />
      </div>
      <div>
        <label>내용:</label>
        <textarea
          value={formData.content}
          onChange={(e) => onChange('content', e.target.value)}
          rows={5}
          style={{ width: '100%', padding: '8px' }}
        />
      </div>
    </div>
  );
}

function PreviewSection({ formData }) {
  return (
    <div style={{ flex: 1 }}>
      <h2>미리보기</h2>
      <div style={{
        border: '1px solid #ddd',
        borderRadius: '8px',
        padding: '20px'
      }}>
        <h3>{formData.title || "제목 없음"}</h3>
        <p style={{ color: '#666' }}>작성자: {formData.author || "익명"}</p>
        <hr />
        <p>{formData.content || "내용을 입력하세요..."}</p>
      </div>
    </div>
  );
}

왼쪽에서 입력하면 오른쪽 미리보기가 실시간으로 바뀌어요!

데이터 흐름 시각화

React의 데이터는 위에서 아래로 흐릅니다 (단방향).

App (State 보유)
↓ Props 전달
Header
↓ Props 전달
Content
↓ Props 전달
List
↓ Props 전달
Details

자식이 부모 State를 바꾸려면:

  1. 부모가 setState 함수를 Props로 전달
  2. 자식이 그 함수를 호출
  3. 부모 State가 바뀌고 새 Props가 자식에게 전달

언제 어디에 State를 둘까?

  1. 해당 컴포넌트만 사용 → 그 컴포넌트에 State
  2. 여러 자식이 공유 → 가장 가까운 공통 부모에 State
  3. 앱 전체에서 사용 → 최상위 컴포넌트 또는 Context/Redux

처음엔 필요한 곳에 두고, 공유가 필요해지면 끌어올리세요.

다음 단계

State와 Props의 관계를 이해했으니, 다음 글에서는 사용자 입력 관리를 다뤄볼게요. 폼 데이터를 어떻게 다루는지, controlled component가 뭔지 배워봅시다.

이 패턴들이 익숙해지면 React가 정말 재밌어져요. 컴포넌트들이 서로 대화하면서 앱이 동작하는 게 보이거든요!


React 시리즈 탐색:

← 블로그 목록으로