리액트의 가장 큰 매력 중 하나는 앱을 설계하는 방식이다. 이번에는 리액트로 상품들을 검색할 수 있는 데이터 테이블을 만드는 과정을 생각해보자.
JSON API와 목업을 디자이너로부터 받았다고 가정하자. 목업과 JSON은 아래와 같다.
[
{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"}
];
첫 번째 할 것은 목업에서 모든 컴포넌트 주변에 박스를 그리고 이름을 붙이는 것이다. 어떤 것이 컴포넌트가 될 지는 우리가 함수나 객체를 만들 때 처럼 만들면 된다. 단일 책임 원칙을 가지고 하나의 컴포넌트가 하나의 일을 하는 것이 이상적이기 때문에 하나의 컴포넌트가 여러가지 일을 수행하거나 커질 경우에는 작은 컴포넌트로 분리되어야 한다. 각 컴포넌트가 JSON 데이터 모델의 한 조각을 나타내도록 분리해야 한다. 위의 앱을 보면 다섯개의 컴포넌트로 이루어져 있고, 컴포넌트 별로 색이 다르다.
위의 컴포넌트를 계층 구조로 나열해보면 다음과 같다.
FilterableProductTable
SearchBar
ProductTable
ProductCategoryRow
ProductRow
컴포넌트의 계층 구조까지 만들고 나서는 앱을 실제로 구현해보면 된다. 가장 쉬운 것은 데이터 모델을 가지고 UI 렌더링은 되는데 아직 아무 동작이 없는 버전을 만드는 것이다. 정적 버전을 만드는 것은 생각은 적게 필요할 수 있지만 타이핑은 많이 필요하다.(하드코딩) 상호작용을 만드는 것은 타이핑은 적지만 생각은 많이 해야한다. 데이터 모델을 렌더링하는 앱의 정적버전을 만들 때는 다른 컴포넌트를 재사용 하는 컴포넌트를 만들고 props를 이용해서 데이터를 전달한다. 정적 버전을 만들때는 state를 사용하지 말아도 된다. state는 오직 상호작용으로 데이터가 바뀔 때 사용하는 것이다. 정적버전에서는 state가 필요 없다.
앱을 만들 때는 하향식(top-down) 또는 상향식(bottom-up)으로 만들 수 있는데, 간단한 예시에서는 보통은 하향식으로 만드는 것이 쉽고 일반적이나, 프로젝트 크기가 클 때는 상향식으로 만들고 테스트를 작성하면서 개발하는 것이 더 나을 수 있다. 이 단계가 마무리되면 재사용 가능한 컴포넌트드르이 라이브러리를 가지게 된다. 현재는 앱의 정적 버전이기 때문에 컴포넌트는 render()
메서드만 있지만 계층구조의 최상단 컴포넌트는 prop으로 데이터 모델을 받는다. 데이터 모델이 변경되면 ReactDOM.render()
을 다시 호출하고 UI가 업데이트 된다. 리액트의 단방향 데이터 흐름은 모든 것을 모듈화 하고 빠르게 만들어 준다.
state와 props의 차이점
props
와state
는 JS 객체이고 두 객체 모두 렌더링 결과물에 영향을 주는 정보를 가지고 있다는 공통점이 있다.props
는 컴포넌트에 전달되는 반면에state
는 (함수 내 선언된 변수처럼) 컴포넌트 안에서 관리된다.
UI에 상호작용을 만드려면 데이터를 변경할 수 있도록 state를 이용해야 한다. 하지만 앱을 잘 만들기 위해서는 변경가능한 state의 최소 집합을 생각해야 한다. 핵심은 중복 배제이다. 필요로 하는 최소한의 state를 찾아서 이것으로 모든 것들이 필요에 따라 그때 그때 계산되도록 해야한다. 만약 TODO 리스트를 만든다면, TODO 아이템을 저장하는 배열만 유지하고 아이템의 개수를 표현하는 state를 별도로 만들 필요는 없다. TODO 갯수를 렌더링한다면 TODO 아이템 배열의 길이를 가져오는 식으로 해야한다.
아까 전 예시 애플리케이션을 생각해보면 다음과 같은 데이터를 가지고 있다.
여기서 어떤 게 state가 되어야 하는지를 정해야 한다.
제품의 원본 목록은 props를 통해 전달되기 때문에 state가 아니다. 검색어나 체크박스는 state로 볼 수 있는데, 시간에 따라 변화하고 다른 걸로 계산될 수 없기 때문이다. 마지막으로 필터링 된 제품들의 목록은 원본 목록과 검색어, 체크박스 값으로 조합해서 계산할 수 있다. 결과적으로 유저가 입력한 검색어와 체크박스의 값이 state를 가지게 된다.
최소한으로 필요한 state를 찾았다면 어떤 컴포넌트가 해당 state를 소유할지를 정해야 한다. 이 부분을 처음에 가장 어려워한다. 아래 과정에 따라 결정해보자.
위의 애플리케이션을 위 과정에 따라 적용해보자.
ProductTable
은 state에 의존한 상품 리스트를 필터링해야 하고 SearchBar
는 검색어와 체크박스의 상태를 표시해야 한다. FilterableProductTable
이다.FilterableProductTable
이 검색어와 체크박스의 체크 여부를 가지는 것이 타당하다.
state를 FilterableProductTable
에 두기로 했다. this.state = {filterText : '', inStockOnly: false}
와 같이 state를 컴포넌트의 constructor
에 추가하고, filterText
와 inStockOnly
를 하위 컴포넌트들에 prop으로 전달하자. 마지막으로는 역방향으로 데이터 흐름을 만드는 것이다. 계층 구조의 하단에 있는 폼 컴포넌트에서 FilterableProductTable
의 state를 업데이트 할 수 있어야 한다. 리액트는 양방향 데이터 바인딩과 비교한다면 더 많은 코딩이 필요하지만 데이터 흐름이 명시적으로 보이게 되기 때문에 프로그램의 동작을 쉽게 파악할 수 있다. 사용자가 폼을 변경할 때 마다 사용자의 입력을 반영할 수 있도록 state가 업데이트 되어야 한다. 컴포넌트는 그 자신의 state만 변경할 수 있기 때문에 SearchBar
에 콜백을 넘겨서 state가 업데이트 되어야 할 때마다 호출되도록 해야한다. input에 onChange 이벤트를 사용함으로써 변화에 대한 알림을 받을 수 있다. FilterableProductTable
에서 전달된 콜백은 setState()
를 호출하고 업데이트가 반영된다.
출처 : 리액트 주요 개념안내서