React로 생각하기
지금까지 각 장에서 하나씩 배웠습니다: 컴포넌트, props, 상태, 이벤트, 리스트. 이 장에서는 이 모든 것이 하나의 방법으로 모이게 됩니다. 디자인이 주어졌을 때, 어떤 것이 컴포넌트인지, 어떤 값이 상태인지, 그 상태가 어디에 있어야 하는지 어떻게 정할까요? 이를 풀어나가는 반복 가능한 방법이 있으며, 몇 번 해보면 거의 모든 화면에서 같은 5단계를 찾게 될 것입니다.
작은 UI를 처음부터 끝까지 따라가겠습니다: 검색 가능한 상품 목록입니다. 상품 목록, 이름으로 필터링하는 검색창, 품절 상품을 숨기는 체크박스가 있습니다. 다음이 렌더링할 데이터입니다:
const PRODUCTS = [
{ id: 1, name: '사과', price: '$1', stocked: true },
{ id: 2, name: '드래곤프루트', price: '$1', stocked: false },
{ id: 3, name: '패션프루트', price: '$2', stocked: true },
]1단계: 디자인을 컴포넌트 트리로 분해하기
디자인을 보고 각 부분 주위에 박스를 그립니다. 좋은 박스는 하나의 일을 하는 것이며, 함수가 뭘 해야 할지 정할 때 쓰는 직관과 같습니다. 우리 화면은 몇 가지로 깔끔하게 나뉩니다:
FilterableProductList전체를 감싼다.SearchBar텍스트 입력창과 품절 상품 체크박스를 가진다.ProductTable상품 목록을 표시한다.ProductRow표의 한 행이다.
이들은 서로 안에 중첩되므로 **컴포넌트 트리**를 이룹니다:
FilterableProductList
├── SearchBar
└── ProductTable
└── ProductRow (상품당 하나)정해진 올바른 분해 방법은 없으며, 컴포넌트 장의 경험 법칙이 여기서도 적용됩니다: 각각을 작고 명확한 일을 하도록 유지합니다. 행은 한 상품을 그리는 방법을 알고, 테이블은 행들을 배치하는 방법을 알고, 검색창은 입력 컨트롤을 압니다. 한 부분이 관련 없는 두 가지 일을 하기 시작하면 보통 분해해야 한다는 신호입니다.
2단계: props만으로 정적 버전 만들기
이제 데이터를 렌더링하지만 아직 아무것도 하지 않는 버전을 만듭니다. 클릭도, 필터링도, 뭔가를 바꾸는 입력도 없습니다. 데이터는 한 방향으로, 트리를 내려가면서 props를 통해 흐릅니다. 이 단계에서는 useState가 나타나지 않습니다.
function FilterableProductList({ products }) {
return (
<div>
<SearchBar />
<ProductTable products={products} />
</div>
)
}
function ProductTable({ products }) {
return (
<table>
<tbody>
{products.map(product => (
<ProductRow key={product.id} product={product} />
))}
</tbody>
</table>
)
}
function ProductRow({ product }) {
return (
<tr>
<td>{product.name}</td>
<td>{product.price}</td>
</tr>
)
}
function SearchBar() {
return (
<form>
<input type="text" placeholder="검색..." />
<label>
<input type="checkbox" /> 품절 상품만 숨기기
</label>
</form>
)
}이것은 매번 전체 목록을 렌더링합니다. 검색창과 체크박스는 화면에 있지만 아직 아무것도 하지 않습니다. 이것이 이 단계의 요점입니다: props만으로 전체 레이아웃이 작동하도록 하고, 1단계의 트리가 정말 함께 동작하는지 확인합니다. 움직이는 부분은 아직 관여하지 않습니다. products 배열은 맨 위에 prop으로 들어와 아래로 전달되고, 여기서는 아무것도 기억하거나 바꾸지 않습니다.
3단계: 최소한의 상태 찾기
이제 상호작용을 만들고, 첫 번째 질문은 어떤 값이 상태여야 하는가입니다. 상태는 올바르게 유지하기 어려우므로 가능한 한 적게 유지하고 싶습니다. 나머지는 계산합니다.
각 값을 테스트하는 것은 간단합니다. 두 가지를 물어봅니다:
- 시간이 지나면서 변하는가?
- 이미 가진 다른 값에서 계산할 수 없는가?
둘 다 그렇다면 상태입니다. 하나라도 아니면 상태가 아닙니다. UI의 값들을 이 테스트를 통해 살펴봅시다:
- 상품 목록. 들어오는 데이터이고 사용자가 페이지를 건드리는 동안 변하지 않으므로 상태가 아닙니다. prop입니다.
- 사용자가 입력한 검색 텍스트. 시간이 지나면서 변하고, 계산할 것이 없습니다. 상태입니다.
- 품절 상품 체크박스가 켜져 있는지 여부. 같은 이야기입니다: 변하고 무엇도 이를 유도하지 않습니다. 상태입니다.
- 화면에 실제로 표시되는 필터링된 목록. 변하지만 상품, 검색 텍스트, 체크박스에서 계산할 수 있습니다. 따라서 상태가 아닙니다.
마지막 것은 명심할 규칙입니다: 파생시키고 복제하지 말기. 보이는 목록은 현재 필터를 통과한 상품이므로 상태의 두 번째 복사본을 저장하는 대신 렌더링 중에 계산합니다:
const visible = products.filter(product => {
const matchesText = product.name
.toLowerCase()
.includes(filterText.toLowerCase())
const matchesStock = !inStockOnly || product.stocked
return matchesText && matchesStock
})visible을 자체 useState에 저장했다면, 상품, 텍스트, 체크박스가 변할 때마다 이를 업데이트하는 것을 기억해야 합니다. 하나라도 빠뜨리면 화면에 오래된 목록이 보입니다. 파생시키는 것은 진실의 유일한 원천이 하나라는 뜻이며, 매 렌더링마다 새로 계산되므로 절대 동기화가 깨질 수 없습니다. 따라서 여기서 최소한의 상태는 정확히 두 개의 값입니다: filterText와 inStockOnly.
4단계: 각 상태 조각이 살 위치 정하기
두 개의 상태 조각이 있습니다. 이제 어떤 컴포넌트가 각각을 소유할지 알아봅시다. 규칙은 상태를 그것이 필요한 모든 컴포넌트의 가장 가까운 공통 부모에 두는 것이며, 이는 상태 올리기 장에서 깊이 있게 다루는 아이디어와 같습니다.
각 값이 누구에게 필요한지 작업해봅시다:
SearchBar는 두 값 모두 필요합니다. 입력창과 이들을 보여주는 체크박스를 렌더링하기 때문입니다.ProductTable도 두 값이 모두 필요합니다. 이들을 사용해 행을 필터링하기 때문입니다.
SearchBar와 ProductTable은 형제입니다. 값은 props를 통해서만 아래로 흐를 수 있으므로, 어느 형제도 다른 형제에게 상태를 전달할 수 없습니다. 둘 다 위에 앉아 있는 가장 가까운 컴포넌트는 FilterableProductList입니다. 상태는 여기로 갑니다:
import { useState } from 'react'
function FilterableProductList({ products }) {
const [filterText, setFilterText] = useState('')
const [inStockOnly, setInStockOnly] = useState(false)
return (
<div>
<SearchBar
filterText={filterText}
inStockOnly={inStockOnly}
onFilterTextChange={setFilterText}
onInStockOnlyChange={setInStockOnly}
/>
<ProductTable
products={products}
filterText={filterText}
inStockOnly={inStockOnly}
/>
</div>
)
}상태는 부모에 살고 값들은 prop으로 두 자식 모두에게 내려갑니다. ProductTable은 이들을 읽어 visible 목록을 만들고, SearchBar는 이들을 읽어 입력창과 체크박스를 채웁니다.
5단계: 상태를 업데이트하는 상호작용 추가하기
값은 자식에게 도달하지만 사용자는 아직 변경할 수 없습니다. 데이터는 아래로만 흐르므로, 변경을 다시 위로 보내려면 setter들을 prop으로 아래에 전달하고 자식이 이들을 호출하도록 합니다. FilterableProductList는 이미 위에서 onFilterTextChange와 onInStockOnlyChange를 전달합니다. SearchBar는 이들을 입력창에 연결합니다:
function SearchBar({
filterText,
inStockOnly,
onFilterTextChange,
onInStockOnlyChange,
}) {
return (
<form>
<input
type="text"
placeholder="검색..."
value={filterText}
onChange={e => onFilterTextChange(e.target.value)}
/>
<label>
<input
type="checkbox"
checked={inStockOnly}
onChange={e => onInStockOnlyChange(e.target.checked)}
/>{' '}
품절 상품만 숨기기
</label>
</form>
)
}이제 루프가 닫혔습니다. 박스에 입력하면 onFilterTextChange를 호출하고, 이는 부모에서 setFilterText입니다. 이는 상태를 업데이트하고, 부모가 다시 렌더링되고, 새로운 filterText가 두 자식 모두에게 다시 내려가고, ProductTable은 visible 목록을 다시 계산하고, 화면이 사용자가 입력한 것과 일치합니다. 체크박스도 같은 방식으로 작동합니다. 상태는 값으로 아래로 가고, 변경은 함수 호출로 다시 위로 올라가고, 파생된 목록은 자체적으로 따라갑니다.
이것이 전부입니다: 컴포넌트 트리를 그리고, props로 정적 버전을 만들고, 최소한의 상태를 찾고, 올바른 소유자에게 올리고, 상호작용을 연결합니다. 거의 모든 화면이 이 5단계에 응합니다.
다음: 접근성 있는 React, 방금 디자인한 인터페이스가 모두를 위해 사용 가능해집니다.

