React 독학 가이드 7 - State와 Props의 관계
Props랑 State를 따로 배웠는데, 둘이 같이 쓰면 진짜 강력해져요. 컴포넌트끼리 데이터를 주고받으면서 복잡한 앱도 만들 수 있거든요.
Props vs State 비교
먼저 둘의 차이를 확실히 정리해볼게요.
| Props | State | |
|---|---|---|
| 누가 관리? | 부모 컴포넌트 | 해당 컴포넌트 |
| 변경 가능? | 읽기 전용 (변경 불가) | 변경 가능 |
| 용도 | 외부에서 받는 데이터 | 내부에서 관리하는 데이터 |
| 변경 시 | 부모가 새 값을 전달해야 함 | 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의 데이터는 위에서 아래로 흐릅니다 (단방향).
자식이 부모 State를 바꾸려면:
- 부모가 setState 함수를 Props로 전달
- 자식이 그 함수를 호출
- 부모 State가 바뀌고 새 Props가 자식에게 전달
언제 어디에 State를 둘까?
- 해당 컴포넌트만 사용 → 그 컴포넌트에 State
- 여러 자식이 공유 → 가장 가까운 공통 부모에 State
- 앱 전체에서 사용 → 최상위 컴포넌트 또는 Context/Redux
처음엔 필요한 곳에 두고, 공유가 필요해지면 끌어올리세요.
다음 단계
State와 Props의 관계를 이해했으니, 다음 글에서는 사용자 입력 관리를 다뤄볼게요. 폼 데이터를 어떻게 다루는지, controlled component가 뭔지 배워봅시다.
이 패턴들이 익숙해지면 React가 정말 재밌어져요. 컴포넌트들이 서로 대화하면서 앱이 동작하는 게 보이거든요!
React 시리즈 탐색:
← 블로그 목록으로