React 처럼 생각하기
리액트 문서 보며 정리
2021-11-20
리액트 문서를 정독하며 정리한 글 영어문서라 조금 힘들었다...
목업으로 시작
다음과 같은 데이터를 받아오는 JSON API가 있다고 가정해보자.
[
{ "category": "Fruits", "price": "$1", "stocked": true, "name": "Apple" },
{ "category": "Fruits", "price": "$1", "stocked": true, "name": "Dragonfruit" },
{ "category": "Fruits", "price": "$2", "stocked": false, "name": "Passionfruit" },
{ "category": "Vegetables", "price": "$2", "stocked": true, "name": "Spinach" },
{ "category": "Vegetables", "price": "$4", "stocked": false, "name": "Pumpkin" },
{ "category": "Vegetables", "price": "$1", "stocked": true, "name": "Peas" }
]
- 목업
1, UI를 컴포넌트로 나누기
- FilterableProductTable (회색)은 전체 앱을 포함합니다.
- SearchBar (파란색)은 사용자 입력을 받습니다.
- ProductTable (라벤더)는 사용자 입력에 따라 목록을 표시하고 필터링합니다.
- ProductCategoryRow (녹색)은 각 범주에 대한 제목을 표시합니다.
- ProductRow (노란색)은 각 제품에 대한 행을 표시합니다.
이를 계층별로 나누면
- FilterableProductTable
- SearchBar
- ProductTable
- ProductCategoryRow
- ProductRow
위와같이 구성된다.
2. React 정적 빌드
이제 위와같은 계층구조를 만들고 앱을 구현할 차례이다. 일반적으로 정적페이지를 만들고 상호작용되는 기능들을 추가하는게 더 쉬울 수 있다. 또 보통은 FilterableProductTable(가장 상위) 부터 컴포넌트에서 하향식으로 작업하는게 쉽지만, 큰 프로젝트에서는 반대로 ProductRow 부터 상향식으로 작업하는게 쉬울 수 있다.
function ProductCategoryRow({ category }) {
return (
<tr>
<th colSpan="2">
{category}
</th>
</tr>
);
}
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 }) {
const rows = [];
let lastCategory = null;
products.forEach((product) => {
if (product.category !== lastCategory) {
rows.push(
<ProductCategoryRow
category={product.category}
key={product.category} />
);
}
rows.push(
<ProductRow
product={product}
key={product.name} />
);
lastCategory = product.category;
});
return (
<table>
<thead>
<tr>
<th>Name</th>
<th>Price</th>
</tr>
</thead>
<tbody>{rows}</tbody>
</table>
);
}
function SearchBar() {
return (
<form>
<input type="text" placeholder="Search..." />
<label>
<input type="checkbox" />
{' '}
Only show products in stock
</label>
</form>
);
}
function FilterableProductTable({ products }) {
return (
<div>
<SearchBar />
<ProductTable products={products} />
</div>
);
}
const PRODUCTS = [
{category: "Fruits", price: "$1", stocked: true, name: "Apple"},
{category: "Fruits", price: "$1", stocked: true, name: "Dragonfruit"},
{category: "Fruits", price: "$2", stocked: false, name: "Passionfruit"},
{category: "Vegetables", price: "$2", stocked: true, name: "Spinach"},
{category: "Vegetables", price: "$4", stocked: false, name: "Pumpkin"},
{category: "Vegetables", price: "$1", stocked: true, name: "Peas"}
];
export default function App() {
return <FilterableProductTable products={PRODUCTS} />;
}
3. 최소화되고 완전한 state 찾기
state를 구조화 하는데 가장 중요한 원칙은 DRY(Don't repeat yourself) 중복배제 이다. 이제 위 애플리케이션의 모든 데이터를 쪼개서 생각해 보자. 제품의 원래 목록 사용자가 입력한 검색 input 체크박스의 값 필터링된 제품 목록 Q. 다음 중 상태는 무엇인가? 시간이 지나도 변하지 않는다면 state가 아니다. 부모를 통해 props로 전달되면 state가 아니다. 기존 컴포넌트의 props나 state기반으로 계산되면 state가 아니다. (이 부분은 해석이 조금 이해되지 않는다.) 다시 살펴 보자. 제품의 원래 목록은 props로 전달되므로 state가 아니다. 검색 input는 시간이 지남에 따라 변경되고 아무 것도 계산할 수 없기 때문에 상태로 보인다. 체크박스의 값은 시간이 지남에 따라 변경되고 아무 것도 계산할 수 없기 때문에 상태로 보인다. 필터링된 제품 목록은 원래 제품 목록을 가져와서 검색 input과 체크박스의 값에 따라 필터링하여 계산할 수 있기 때문에 상태가 아니다.
4. state가 어디에 있어야 하는지 확인
React는 단방향 데이터 흐름을 사용하여 상위 컴포넌트에서 하위 컴포넌트로 컴포넌트 계층 구조를 따라 데이터를 전달한다. 위에서 아래로!
- 애플리케이션의 각 state에 대해:
- state를 기반으로 무언가를 렌더링 하는 모든 컴포넌트를 확인한다.
- 가장 가까운 부모 컴포넌트를 찾는다.
- state가 어디에 있어야 하는지 결정:
- state를 바로 위 부모 컴포넌트에 놓을 수 있다.
- 또한 state를 공통된 부모 컴포넌트의 부모 컴포넌트에 놓을 수 있다.
- state를 놓을 곳을 찾을 수 없으면 state를 유지하기 위한 새 컴포넌트를 만들고 최상위 컴포넌트 어딘가에 추가한다.
- 이전 단계에서 이 애플리케이션에서 검색 기능과 체크박스의 두 가지 상태를 찾았고, 이 예시 에서는 항상 함께 나타나므로 단일 상태로 생각하는 것이 더 쉽다.
- state를 사용하는 컴포넌트 찾기:
- ProductTable의 state(검색 및 체크박스 값)를 기반으로 제품 목록을 필터링해야 한다.
- SearchBar의 state(검색 및 체크박스 값)를 표시해야 한다.
- 공통되는 상위 컴포넌트 찾기: 두 컴포넌트가 공유하는 첫 번째 부모 컴포넌트는 FilterableProductTable.(두 컴포넌트의 상위 부모 컴포넌트)
- state가 있는 위치 결정 : 검색 및 체크박스의 state 값을 FilterableProductTable(두 컴포넌트의 상위 부모 컴포넌트)에서 유지해야한다.
- 따라서 state는 FilterableProductTable(두 컴포넌트의 상위 부모 컴포넌트)에 있어야 한다.
function FilterableProductTable({ products }) {
const [filterText, setFilterText] = useState('');
const [inStockOnly, setInStockOnly] = useState(false);
그런 다음 props로 내려준다
<div>
<SearchBar
filterText={filterText}
inStockOnly={inStockOnly} />
<ProductTable
products={products}
filterText={filterText}
inStockOnly={inStockOnly} />
</div>
5. 반대방향의 데이터 흐름 추가하기
마지막으로는 SearchBar, ProductTable에서 입력받은 state를 FilterableProductTable로 다시 전달해 줘야한다.
function FilterableProductTable({ products }) {
const [filterText, setFilterText] = useState('');
const [inStockOnly, setInStockOnly] = useState(false);
return (
<div>
<SearchBar
filterText={filterText}
inStockOnly={inStockOnly}
onFilterTextChange={setFilterText}
onInStockOnlyChange={setInStockOnly} />
// SearchBar
<input
type="text"
value={filterText}
placeholder="Search..."
onChange={(e) => onFilterTextChange(e.target.value)} />