1편에 이어서 2편! 열심히 또 적어서 정리하기로~!! 🤓
UI를 상호작용하게 만드려면 데이터 모델을 변경할 수 있는 방법이 있어야하는데, 이를 React는 state를 통해 변경한다. 애플리케이션에서 필요로 하는 변경 가능한 state의 최소 집합을 생각해야하는데 여기서 핵심은 중복배제원칙(= 중복은 피하라는 말? 같다)이다.
애플리케이션이 필요로 하는 가장 최소한의 state를 찾고 나머지 모든 것들이 필요에 따라 계산되게하자! TO DO List로 예를들면 아이템을 저장하는 배열만 유지하고 아이템 갯수를 표현하는 state를 별도로 만들지않아야한다. TO DO 갯수를 렌더링해야한다면 아이템 배열의 길이를 가져오면 된다.(= 굳이 갯수를 가져오는 state를 만들지 말고 배열의 길이를 가져오라는 말)
예시 애플리케이션 내 데이터들을 생각해보자 다음과 같은 데이터를 가지고있을것이다.
각각 어떤것이 state가 되어야 할지 생각해야한다면 아래의 세 가지 질문을 통해 결정할 수 있다.
제품의 원본 목록은 props를 통해 전달되므로 state가 아니다. 검색어와 체크박스는 state로 볼 수 있는데 시간이 지남에 따라 볂기도 하면서 다른 것들로부터 계산될 수 없기 때문이다. 그리고 필터링된 목록은 state가 아니다. 제품의 원본 목록과 검색어, 체크박스의 값을 조합해 계산해낼 수 있기 때문이다.
결과적으로 애플리케이션은 다음과 같은 state를 가진다.
class ProductCategoryRow extends React.Component {
render() {
const category = this.props.category;
return (
<tr>
<th colSpan="2">
{category}
</th>
</tr>
);
}
}
class ProductRow extends React.Component {
render() {
const product = this.props.product;
const name = product.stocked ?
product.name :
<span style={{color: 'red'}}>
{product.name}
</span>;
return (
<tr>
<td>{name}</td>
<td>{product.price}</td>
</tr>
);
}
}
class ProductTable extends React.Component {
render() {
const filterText = this.props.filterText;
const inStockOnly = this.props.inStockOnly;
const rows = [];
let lastCategory = null;
this.props.products.forEach((product) => {
if (product.name.indexOf(filterText) === -1) {
return;
}
if (inStockOnly && !product.stocked) {
return;
}
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>
);
}
}
class SearchBar extends React.Component {
render() {
const filterText = this.props.filterText;
const inStockOnly = this.props.inStockOnly;
return (
<form>
<input
type="text"
placeholder="Search..."
value={filterText} />
<p>
<input
type="checkbox"
checked={inStockOnly} />
{' '}
Only show products in stock
</p>
</form>
);
}
}
class FilterableProductTable extends React.Component {
constructor(props) {
super(props);
this.state = {
filterText: '',
inStockOnly: false
};
}
render() {
return (
<div>
<SearchBar
filterText={this.state.filterText}
inStockOnly={this.state.inStockOnly}
/>
<ProductTable
products={this.props.products}
filterText={this.state.filterText}
inStockOnly={this.state.inStockOnly}
/>
</div>
);
}
}
const PRODUCTS = [
{category: 'Sporting Goods', price: '$49.99', stocked: true, name: 'Football'},
{category: 'Sporting Goods', price: '$9.99', stocked: true, name: 'Baseball'},
{category: 'Sporting Goods', price: '$29.99', stocked: false, name: 'Basketball'},
{category: 'Electronics', price: '$99.99', stocked: true, name: 'iPod Touch'},
{category: 'Electronics', price: '$399.99', stocked: false, name: 'iPhone 5'},
{category: 'Electronics', price: '$199.99', stocked: true, name: 'Nexus 7'}
];
ReactDOM.render(
<FilterableProductTable products={PRODUCTS} />,
document.getElementById('container')
);
최소한으로 필요한 state가 뭔지 알아냈다면 어떤 컴포넌트가 state를 변경하거나 소유할지 찾아야한다. React는 단방향 데이터 흐름을 따른다. 어떤 컴포넌트가 어떤 state를 가져야하는지 결정하기 어렵다면 아래 과정을 따라 결정하자
이제 이 내용을 가지고 만들어둔 애플리케이션에 적용해보자
이제 state를 FilterableProductTable 두기로 정했다. 먼저 인스턴스 속성인 this.state = {filterText: '', inStockOnly: false} 를 FilterableProductTable의 constructor에 추가하여 애플리케이션의 초기 상태를 반영한다. 그리고 나서 filterText와 inStockOnly를 ProductTable와 SearchBar에 prop으로 전달합니다. 마지막으로 이 props를 사용하여 ProductTable의 행을 정렬하고 SearchBar의 폼 필드 값을 설정하자
이제 애플리케이션의 동작을 볼 수 있다. filterText를 "ball"로 설정하고 앱을 새로고침하면 데이터 테이블이 올바르게 업데이트 된 것을 볼 수 있다.
class ProductCategoryRow extends React.Component {
render() {
const category = this.props.category;
return (
<tr>
<th colSpan="2">
{category}
</th>
</tr>
);
}
}
class ProductRow extends React.Component {
render() {
const product = this.props.product;
const name = product.stocked ?
product.name :
<span style={{color: 'red'}}>
{product.name}
</span>;
return (
<tr>
<td>{name}</td>
<td>{product.price}</td>
</tr>
);
}
}
class ProductTable extends React.Component {
render() {
const filterText = this.props.filterText;
const inStockOnly = this.props.inStockOnly;
const rows = [];
let lastCategory = null;
this.props.products.forEach((product) => {
if (product.name.indexOf(filterText) === -1) {
return;
}
if (inStockOnly && !product.stocked) {
return;
}
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>
);
}
}
class SearchBar extends React.Component {
constructor(props) {
super(props);
this.handleFilterTextChange = this.handleFilterTextChange.bind(this);
this.handleInStockChange = this.handleInStockChange.bind(this);
}
handleFilterTextChange(e) {
this.props.onFilterTextChange(e.target.value);
}
handleInStockChange(e) {
this.props.onInStockChange(e.target.checked);
}
render() {
return (
<form>
<input
type="text"
placeholder="Search..."
value={this.props.filterText}
onChange={this.handleFilterTextChange}
/>
<p>
<input
type="checkbox"
checked={this.props.inStockOnly}
onChange={this.handleInStockChange}
/>
{' '}
Only show products in stock
</p>
</form>
);
}
}
class FilterableProductTable extends React.Component {
constructor(props) {
super(props);
this.state = {
filterText: '',
inStockOnly: false
};
this.handleFilterTextChange = this.handleFilterTextChange.bind(this);
this.handleInStockChange = this.handleInStockChange.bind(this);
}
handleFilterTextChange(filterText) {
this.setState({
filterText: filterText
});
}
handleInStockChange(inStockOnly) {
this.setState({
inStockOnly: inStockOnly
})
}
render() {
return (
<div>
<SearchBar
filterText={this.state.filterText}
inStockOnly={this.state.inStockOnly}
onFilterTextChange={this.handleFilterTextChange}
onInStockChange={this.handleInStockChange}
/>
<ProductTable
products={this.props.products}
filterText={this.state.filterText}
inStockOnly={this.state.inStockOnly}
/>
</div>
);
}
}
const PRODUCTS = [
{category: 'Sporting Goods', price: '$49.99', stocked: true, name: 'Football'},
{category: 'Sporting Goods', price: '$9.99', stocked: true, name: 'Baseball'},
{category: 'Sporting Goods', price: '$29.99', stocked: false, name: 'Basketball'},
{category: 'Electronics', price: '$99.99', stocked: true, name: 'iPod Touch'},
{category: 'Electronics', price: '$399.99', stocked: false, name: 'iPhone 5'},
{category: 'Electronics', price: '$199.99', stocked: true, name: 'Nexus 7'}
];
ReactDOM.render(
<FilterableProductTable products={PRODUCTS} />,
document.getElementById('container')
);
우리는 지금까지 계층 구조가 아래로 흐르는 props와 state의 함수로써 앱을 만들었다. 이제 다른 방향의 데이터 흐름을 만들어볼 시간이다!(😩이게 너무 어렵다..) 계층 구조의 하단에 있는 폼 컴포넌트에서 FilterableProductTable의 state를 업데이터할 수 있어야 한다.
React는 양방향 데이터 바인딩(two-way data binding)과 비교하면 더 많은 타이핑을 필요로 하지만 데이터 흐름을 명시적으로 보이게 만들어 프로그램이 어떻게 동작하는지 파악할 수 있게 도와준다.
현재 input box를 체크하거나 키보드를 타이핑할 경우 React가 입력을 무시하는 것을 확인할 수 있다. 이는 input 태그의 value속성이 항상 FilterableProductTable에서 전달된 state와 동일하도록 설정했기 때문이다.
우리가 원하는 것이 무엇인지 생각해보자. 우리는 사용자가 폼을 변경할 때마다 사용자의 입력을 반영할 수 있도록 state를 업데이트하기 원한다. 컴포넌트는 자신의 state만 변경할 수 있기 때문에 FilterableProductTable는 SearchBar에 콜백을 넘겨서 state가 업데이트되어야 할 때마다 호출되도록 할 것이다. 우리는 input에 onChange 이벤트를 사용해서 알림을 받을 수 있다. FilterableProductTable에서 전달된 콜백은 setState()를 호출하고 앱이 업데이트될 것이다.
이로서 설명은 끝이났다.. react랑 친해지려면 5단계를 생각하고 state 사용방법을 꼭 한번 더 생각 해봐야겠다! 쉽지 않겠지만 친해지면 좋은 친구인 react 1개국어를 발휘할 수 있을때까지 뽜이팅!!🤨