React를 배울 때 문법보다 헷갈리는 것이 있다. 화면을 어디까지 컴포넌트로 쪼개야 하는지, 무엇을 State로 만들어야 하는지, 그 State를 어느 컴포넌트에 둬야 하는지다.
React 공식문서의 ‘React로 사고하기’는 이 문제를 검색 가능한 상품 테이블 하나로 설명한다. 원문은 길지만 결론은 다섯 단계다.
- 화면을 컴포넌트로 쪼갠다.
- 데이터만 받아 그리는 정적 화면부터 만든다.
- 꼭 필요한 최소한의 State를 찾는다.
- State를 소유할 컴포넌트를 정한다.
- 자식의 입력으로 부모 State를 변경하게 만든다.
하나씩 보자.
예제: 검색 가능한 상품 테이블
서버에서 다음과 같은 상품 목록을 받는다고 하자.
const products = [
{ category: '과일', price: '1,000원', stocked: true, name: '사과' },
{ category: '과일', price: '2,000원', stocked: false, name: '망고' },
{ category: '채소', price: '1,500원', stocked: true, name: '시금치' },
];
만들 화면에는 검색창, 재고 상품만 보는 체크박스, 카테고리별 상품 표가 있다. 검색어를 입력하면 상품이 걸러지고, 체크박스를 누르면 재고가 있는 상품만 보여야 한다.
1단계: 화면을 컴포넌트 계층으로 쪼갠다
먼저 화면의 각 영역에 박스를 친다고 생각하면 쉽다.
FilterableProductTable: 검색창과 상품 표를 감싸는 전체 화면SearchBar: 검색어와 재고 여부를 입력받는 영역ProductTable: 필터링된 상품 목록을 보여주는 표ProductCategoryRow: 과일, 채소 같은 카테고리 행ProductRow: 개별 상품 행
계층으로 표현하면 다음과 같다.
FilterableProductTable
├─ SearchBar
└─ ProductTable
├─ ProductCategoryRow
└─ ProductRow
컴포넌트를 나눌 때는 한 컴포넌트가 한 가지 역할을 맡는가를 보면 된다. 다만 작은 HTML 조각까지 무조건 컴포넌트로 만들 필요는 없다. 단순한 테이블 헤더라면 ProductTable 안에 그대로 두고, 정렬이나 필터 같은 별도 기능이 생길 때 분리해도 늦지 않다.
데이터 구조도 힌트가 된다. 데이터가 카테고리와 상품으로 나뉜다면 UI 역시 카테고리 행과 상품 행으로 나누는 것이 자연스럽다.
2단계: 상호작용 없는 정적 화면부터 만든다
컴포넌트를 나눴다면 검색 기능부터 붙이지 말고, 우선 상품 데이터가 화면에 제대로 나오게 만든다.
function ProductRow({ product }) {
const name = product.stocked
? product.name
: <span style={{ color: 'red' }}>{product.name}</span>;
return (
<tr>
<td>{name}</td>
<td>{product.price}</td>
</tr>
);
}
function ProductTable({ products }) {
return (
<table>
<thead>
<tr>
<th>상품명</th>
<th>가격</th>
</tr>
</thead>
<tbody>
{products.map(product => (
<ProductRow key={product.name} product={product} />
))}
</tbody>
</table>
);
}
이 단계에서는 Props만 사용한다. 부모가 데이터를 받고 자식에게 내려주면, 자식은 받은 데이터로 화면을 그린다.
function FilterableProductTable({ products }) {
return (
<div>
<SearchBar />
<ProductTable products={products} />
</div>
);
}
데이터가 부모에서 자식으로 내려가는 이 구조를 단방향 데이터 흐름이라고 한다.
정적 화면을 만들 때는 State를 쓰지 않는다. 아직 사용자의 행동에 따라 바뀌는 값이 없기 때문이다. 정적인 화면과 상호작용을 동시에 만들면 데이터 전달 문제와 State 문제를 한꺼번에 해결해야 해서 오히려 복잡해진다.
3단계: 정말 필요한 State만 찾는다
이제 검색과 체크박스를 동작시켜야 한다. 이때 화면에 등장하는 모든 값을 State로 만들면 안 된다.
이 예제에는 네 가지 데이터가 있다.
- 원본 상품 목록
- 사용자가 입력한 검색어
- ‘재고 상품만 보기’ 체크 여부
- 필터링된 상품 목록
State인지 판단할 때는 세 가지를 묻는다.
- 시간이 지나도 변하지 않는가? 그렇다면 State가 아니다.
- 부모에게 Props로 받는가? 그렇다면 State가 아니다.
- 다른 Props나 State로 계산할 수 있는가? 그렇다면 State가 아니다.
이 기준으로 걸러보면 다음과 같다.
| 데이터 | State 여부 | 이유 |
|---|---|---|
| 원본 상품 목록 | 아니오 | 부모에게 Props로 받는다. |
| 검색어 | 예 | 사용자가 바꾸며 다른 값으로 계산할 수 없다. |
| 재고 체크 여부 | 예 | 사용자가 바꾸며 다른 값으로 계산할 수 없다. |
| 필터링된 상품 목록 | 아니오 | 상품 목록, 검색어, 체크 여부로 계산할 수 있다. |
따라서 State는 filterText와 inStockOnly 두 개뿐이다.
const [filterText, setFilterText] = useState('');
const [inStockOnly, setInStockOnly] = useState(false);
특히 계산할 수 있는 값을 State에 중복 저장하지 않는 것이 중요하다. 필터링된 목록까지 State로 저장하면 원본이나 검색 조건이 바뀔 때마다 함께 맞춰줘야 한다. 언젠가는 셋 중 하나가 어긋난다.
const visibleProducts = products.filter(product => {
const matchesText = product.name
.toLowerCase()
.includes(filterText.toLowerCase());
const matchesStock = !inStockOnly || product.stocked;
return matchesText && matchesStock;
});
필터링 결과는 렌더링할 때 계산하면 된다.
4단계: State를 어디에 둘지 정한다
State를 찾았으면 누가 그 값을 소유할지 정해야 한다. 기준은 해당 State를 사용하는 컴포넌트들의 가장 가까운 공통 부모다.
이 예제에서는 다음 두 컴포넌트가 검색 조건을 사용한다.
SearchBar: 현재 검색어와 체크 상태를 입력창에 표시한다.ProductTable: 검색어와 체크 상태에 맞춰 상품을 필터링한다.
둘의 가장 가까운 공통 부모는 FilterableProductTable이다. 따라서 State를 여기에 둔다.
function FilterableProductTable({ products }) {
const [filterText, setFilterText] = useState('');
const [inStockOnly, setInStockOnly] = useState(false);
return (
<div>
<SearchBar
filterText={filterText}
inStockOnly={inStockOnly}
/>
<ProductTable
products={products}
filterText={filterText}
inStockOnly={inStockOnly}
/>
</div>
);
}
부모가 State를 소유하고 필요한 자식들에게 Props로 내려준다. 만약 적당한 공통 부모가 없다면 State를 관리할 새 부모 컴포넌트를 만들 수도 있다.
5단계: 자식의 입력으로 부모 State를 바꾼다
현재 SearchBar는 값을 받아서 보여주기만 한다.
<input value={filterText} />
이 상태에서는 입력창에 타이핑해도 값이 바뀌지 않는다. value가 부모 State로 고정되어 있는데, 그 State를 변경하는 코드가 없기 때문이다. React에서는 이런 입력을 제어 컴포넌트라고 한다.
State는 부모가 가지고 있으므로 변경 함수도 부모에게 있다. 부모는 setFilterText와 setInStockOnly를 자식에게 Props로 내려준다.
<SearchBar
filterText={filterText}
inStockOnly={inStockOnly}
onFilterTextChange={setFilterText}
onInStockOnlyChange={setInStockOnly}
/>
자식은 사용자의 입력이 발생했을 때 전달받은 함수를 호출한다.
function SearchBar({
filterText,
inStockOnly,
onFilterTextChange,
onInStockOnlyChange,
}) {
return (
<form>
<input
type="text"
value={filterText}
placeholder="상품 검색"
onChange={event => onFilterTextChange(event.target.value)}
/>
<label>
<input
type="checkbox"
checked={inStockOnly}
onChange={event => onInStockOnlyChange(event.target.checked)}
/>
재고 상품만 보기
</label>
</form>
);
}
여기서 데이터의 움직임을 보면 React의 구조가 보인다.
- 부모가 현재 State를 Props로 자식에게 내려준다.
- 자식은 Props로 입력창을 렌더링한다.
- 사용자가 입력하면 자식의
onChange가 실행된다. - 자식이 부모에게 받은 변경 함수를 호출한다.
- 부모 State가 바뀌고 화면이 다시 렌더링된다.
값은 부모에서 자식으로 내려가고, 변경 요청은 함수 호출을 통해 자식에서 부모로 올라간다. 데이터 자체가 양방향으로 자동 연결되는 것이 아니라서 코드가 조금 길어지지만, 어디서 값이 왔고 누가 바꿨는지 명확하다는 장점이 있다.
전체 코드를 한 번에 보면
핵심만 합치면 다음과 같다.
import { useState } from 'react';
export default function FilterableProductTable({ products }) {
const [filterText, setFilterText] = useState('');
const [inStockOnly, setInStockOnly] = useState(false);
const visibleProducts = products.filter(product => {
const matchesText = product.name
.toLowerCase()
.includes(filterText.toLowerCase());
const matchesStock = !inStockOnly || product.stocked;
return matchesText && matchesStock;
});
return (
<div>
<SearchBar
filterText={filterText}
inStockOnly={inStockOnly}
onFilterTextChange={setFilterText}
onInStockOnlyChange={setInStockOnly}
/>
<ProductTable products={visibleProducts} />
</div>
);
}
function SearchBar({
filterText,
inStockOnly,
onFilterTextChange,
onInStockOnlyChange,
}) {
return (
<>
<input
value={filterText}
onChange={event => onFilterTextChange(event.target.value)}
placeholder="상품 검색"
/>
<label>
<input
type="checkbox"
checked={inStockOnly}
onChange={event => onInStockOnlyChange(event.target.checked)}
/>
재고 상품만 보기
</label>
</>
);
}
function ProductTable({ products }) {
return (
<table>
<tbody>
{products.map(product => (
<tr key={product.name}>
<td>{product.name}</td>
<td>{product.price}</td>
</tr>
))}
</tbody>
</table>
);
}
공식문서 예제보다 카테고리 렌더링은 단순화했지만, React로 사고하는 핵심 구조는 모두 들어 있다.
결국 React로 사고한다는 것
React로 사고한다는 것은 화면을 보자마자 useState부터 만드는 일이 아니다.
먼저 화면과 데이터를 컴포넌트 단위로 나누고, Props만으로 정적인 화면을 만든다. 그다음 사용자 행동에 따라 실제로 변해야 하는 최소한의 값만 State로 고른다. State를 사용하는 컴포넌트들의 공통 부모가 그 값을 소유하고, 자식에게는 값과 변경 함수를 Props로 내려준다.
한 문장으로 줄이면 이렇다.
UI를 컴포넌트로 쪼개고, 최소한의 State를 공통 부모에 둔 뒤, 데이터는 아래로 보내고 변경 요청은 위로 올린다.
이 기준만 잡혀도 React 코드에서 State가 여기저기 흩어지거나, 같은 데이터를 여러 State에 중복 저장하거나, 자식과 부모의 값이 서로 어긋나는 문제를 상당히 줄일 수 있다.
'프레임워크 > JavaScript, React' 카테고리의 다른 글
| React 컴파일러 (0) | 2026.08.15 |
|---|---|
| 새로운 React 앱 만들기 (0) | 2026.08.12 |
| react 시작하기 (0) | 2026.08.12 |
| 반응형 웹 디자인 (0) | 2024.09.02 |
| React Router와 TypeScript를 활용한 프로젝트 가이드 (0) | 2024.06.25 |