12: React로 생각하기

29970 단어 React공식문서React

React로 생각하기

React는 JavaScript로 규모가 크고 빠른 웹 애플리케이션을 만드는 가장 좋은 방법이다.
React는 Facebook과 Instagram을 통해 확장성을 입증했다.

  • React의 가장 멋진 점 중 하나는 앱을 설계하는 방식이다.
  • 이 문서를 통해 React로 상품들을 검색할 수 있는 데이터 테이블을 만드는 과정을 함께 생각해 보자.

목업으로 시작하기

목업이란 프로토타입, 시제품과 같은 말이다.

  • JSON API와 목업을 디자이너로부터 받았다고 가정해 보자.
  • 목업은 아마 아래와 같을 것이다.
  • JSON API같은 경우 아래와 같은 데이터를 반환한다.
[
  {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"}
];

1단계: UI를 컴포넌트 계층 구조로 나누기

  • 가장 먼저 해야할 일은 모든 컴포넌트(와 하위 컴포넌트)의 주변에 박스를 그리고 그 각각에 이름을 붙이는 것이다.
  • 디자이너와 함께 일한다면, 이것들을 이미 정해두었을 수 있으니 한번 대화해보자
  • 디자이너의 포토샵 레이어 이름이 React 컴포넌트의 이름이 될 수 있다.
  • 하지만 어떤 것이 컴포넌트가 되어야 할지 어떻게 알 수 있을까?
  • 새로운 함수나 객체를 만들 때처럼 만들면 된다.
  • 한 가지 테크닉은 단일 책임 원칙이다.
  • 단일 책임 원칙은 하나의 컴포넌트는 한 가지 일을 하는게 이상적이라는 원칙이다.
  • 하나의 컴포넌트가 커지게 되면 이 컴포넌트는 보다 작은 하위 컴포넌트들로 분리되어야 한다.
  • 유저들에게 보여주는 데이터는 주로 JSON 데이터이다.

  • 그러므로 데이터 모델이 적절하게 만들어졌다면, UI(컴포넌트 구조)가 잘 연결될 것이다.

  • 이는 UI와 데이터 모델이 같은 인포메이션 아키텍처(information architecture)를 가지는 경향이 있기 때문이다.

  • 각 컴포넌트가 데이터 모델의 한 조각을 나타내도록 분리하자.

  • 위 앱은 5개의 컴포넌트로 나누었다.

  • 각각의 컴포넌트에 들어간 데이터는 굵은 글씨이탤릭체로 표기했다.

    1. FilterableProductTable(노란색): 예시 전체를 포괄한다.
    2. SearchBar(파란색): 모든 유저의 입력(user input) 을 받는다.
    3. ProductTable(연두색): 유저의 입력(user input) 을 기반으로 데이터 콜렉션(data collection) 을 필터링 해서 보여준다.
    4. ProductCategoryRow(하늘색): 각 카테고리(category) 의 헤더를 보여준다.
    5. ProductRow(빨강색): 각각의 제품(product) 에 해당하는 행을 보여준다.
  • ProductTable(전체 앱)을 보면 “Name” 과 “Price” 레이블을 포함한 테이블 헤더만을 가진 컴포넌트는 없다.
  • 이 같은 경우, 데이터를 위한 독립된 컴포넌트를 생성할지 생성하지 않을지는 선택이다.
  • 이 예시에서는 ProductTable의 책임인 데이터 컬렉션(data collection)이 렌더링의 일부이기 때문에 ProductTable을 남겨두었다.
  • 그러나 이 헤더가 복잡해지면 (정렬을 위한 기능을 추가하는 등) ProductTableHeader컴포넌트를 만드는 것이 더 합리적일 것이다.
  • 쉽게 말해 기능을 추가하며 컴포넌트가 복잡해지면 세분화 하는게 더 합리적인 판단이다.
  • 이제 목업에서 컴포넌트를 확인하였으니 이 목업을 계층 구조로 나열해보자.
  • 모형의 다른 컴포넌트 내부에 나타나는 컴포넌트는 계층 구조의 자식으로 나타낸다.
  • FilterableProductTable
    • SearchBar
    • ProductTable
      • ProductCategoryRow
      • ProductRow

2단계: React로 정적인 버전 만들기

CodePen에서 리액트로 생각하기: 2단계 를 살펴보자

  • 이제 컴포넌트 계층구조가 만들어졌으니 앱을 실제로 구현해볼 시간이다.
  • 가장 쉬운 방법은 데이터 모델을 가지고 UI를 렌더링은 되지만 아무 동작도 없는 버전을 만들어보는 것이다.
  • 이처럼 과정을 나누는 것이 좋은데 정적 버전을 만드는 것은 생각은 적게 필요하지만 타이핑은 많이 필요로 하고, 상호작용을 만드는 것은 생각은 많이 해야 하지만 타이핑은 적게 필요로 하기 때문이다.
  • 나중에 좀 더 자세히 살펴보자.
  • 데이터 모델을 렌더링하는 앱의 정적 버전을 만들기 위해 다른 컴포넌트를 재사용하는 컴포넌트를 만들고 props 를 이용해 데이터를 전달해주자.
  • props는 부모가 자식에게 데이터를 넘겨줄 때 사용할 수 있는 방법이다.
  • 정적 버전을 만들기 위해 state를 사용하지 말자.
  • state는 오직 상호작용을 위해, 즉 시간이 지남에 따라 데이터가 바뀌는 것에 사용한다.
  • 우리는 앱의 정적 버전을 만들고 있기 때문에 지금은 필요하지 않다.
  • 앱을 만들 때 하향식(top-down)이나 상향식(bottom-up)으로 만들 수 있다.
  • 다시 말해 계층 구조의 상층부에 있는 컴포넌트 (즉 FilterableProductTable부터 시작하는 것)부터 만들거나 하층부에 있는 컴포넌트 (ProductRow) 부터 만들 수도 있다.
  • 간단한 예시에서는 보통 하향식으로 만드는 게 쉽지만 프로젝트가 커지면 상향식으로 만들고 테스트를 작성하면서 개발하는게 더 쉽다.
  • 이 단계가 끝나면 데이터 렌더링을 위해 만들어진 재사용 가능한 컴포넌트들의 라이브러리를 가지게 된다.
  • 현재는 앱의 정적 버전이기 때문에 컴포넌트는 render() 메서드만 가지고 있을 것이다.
  • 계층구조의 최상단 컴포넌트 (FilterableProductTable)는 prop으로 데이터 모델을 받는다.
  • 데이터 모델이 변경되면 ReactDOM.render()를 다시 호출해서 UI가 업데이트 된다.
  • UI가 어떻게 업데이트되고 어디에서 변경해야하는지 알 수 있다.
  • React의 단방향 데이터 흐름(one-way data flow) (또는 단방향 바인딩(one-way binding))는 모든 것을 모듈화 하고 빠르게 만들어준다.
import React from 'react';
import ReactDOM from 'react-dom';
// 카테고리 (하늘색)
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 rows = [];
    let lastCategory = null;
    this.props.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>
    );
  }
}
// 서치바(파란색)
class SearchBar extends React.Component {
  render() {
    return (
      <form>
        <input type="text" placeholder="Search..." />
        <p>
          <input type="checkbox" />
          {' '}
          Only show products in stock
        </p>
      </form>
    );
  }
}
// 래퍼 컴포넌트 (노란색)
class FilterableProductTable extends React.Component {
  render() {
    return (
      <div>
        <SearchBar />
        <ProductTable products={this.props.products} />
      </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('root')
);

3단계: UI state에 대한 최소한의 (하지만 완전한) 표현 찾아내기

  • UI를 상호작용하게 만들려면 기반 데이터 모델을 변경할 수 있는 방법이 있어야 한다.
  • 이를 React는 state를 통해 변경한다.
  • 애플리케이션을 올바르게 만들기 위해서는 애플리케이션에서 필요로 하는 변경 가능한 state의 최소 집합을 생각해보아야 한다.
  • 여기서 핵심은 중복배제원칙이다.
  • 애플리케이션이 필요로 하는 가장 최소한의 state를 찾고 이를 통해 나머지 모든 것들이 필요에 따라 그때그때 계산되도록 만들자.
  • 예를 들어 TODO 리스트를 만든다고 하면, TODO 아이템을 저장하는 배열만 유지하고 TODO 아이템의 개수를 표현하는 state를 별도로 만들지 말자.
  • TODO 갯수를 렌더링해야한다면 TODO 아이템 배열의 길이를 가져오면 된다.
  • 예시 애플리케이션 내 데이터들을 생각해보자. 애플리케이션은 다음과 같은 데이터를 가지고 있다.
    • 제품의 원본 목록
    • 유저가 입력한 검색어
    • 체크박스의 값
    • 필터링 된 제품들의 목록
  • 각각 살펴보고 어떤 게 state가 되어야 하는 지 살펴보자.
  • 이는 각 데이터에 대해 아래의 세 가지 질문을 통해 결정할 수 있다.
    1. 부모로부터 props를 통해 전달되면 확실히 state가 아니다.
    2. 시간이 지나도 변하지 않으면 확실히 state가 아니다.
    3. 컴포넌트 안의 다른 state나 props를 가지고 계산 가능하다면 그렇다면 state가 아니다.
  • 제품의 원본 목록은 props를 통해 전달되므로 state가 아니다.
  • 검색어와 체크박스는 state로 볼 수 있는데 시간이 지남에 따라 변하기도 하면서 다른 것들로부터 계산될 수 없기 때문이다.
  • 그리고 마지막으로 필터링된 목록은 state가 아니다. 제품의 원본 목록과 검색어, 체크박스의 값을 조합해서 계산해낼 수 있기 때문이다.
  • 결과적으로 애플리케이션은 다음과 같은 state를 가진다.
    • 유저가 입력한 검색어
    • 체크박스의 값

4단계: State가 어디에 있어야 할 지 찾기

CodePen에서 리액트로 생각하기: 4단계 살펴보기

  • 이제 앱에서 최소한으로 필요한 state가 뭔지 찾아냈다.

  • 다음으로는 어떤 컴포넌트가 state를 변경하거나 소유할지 찾아야 한다.

    기억할점: React는 항상 컴포넌트 계층구조를 따라 아래로 내려가는 단방향 데이터 흐름을 따른다. 어떤 컴포넌트가 어떤 state를 가져야 하는 지 바로 결정하기 어려울 수 있다. 많은 초보자가 이 부분을 가장 어려워한다. 아래 과정을 따라 결정해 보자.

  • 애플리케이션이 가지는 각각의 state에 대해서

    • state를 기반으로 렌더링하는 모든 컴포넌트를 찾자.
    • 공통 소유 컴포넌트 (common owner component)를 찾자. (계층 구조 내에서 특정 state가 있어야 하는 모든 컴포넌트들의 상위에 있는 하나의 컴포넌트).
    • 공통 혹은 더 상위에 있는 컴포넌트가 state를 가져야 한다.
    • state를 소유할 적절한 컴포넌트를 찾지 못하였다면, state를 소유하는 컴포넌트를 하나 만들어서 공통 오너 컴포넌트의 상위 계층에 추가하자.
  • 이 전략을 애플리케이션에 적용해보자.

    • ProductTable은 state에 의존한 상품 리스트의 필터링해야 하고 SearchBar는 검색어와 체크박스의 상태를 표시해주어야 한다.
    • 공통 소유 컴포넌트는 FilterableProductTable이다.
    • 의미상으로도 FilterableProductTable이 검색어와 체크박스의 체크 여부를 가지는 것이 타당하다.
  • 이제 state를 FilterableProductTable에 두기로 했다.

  • 먼저 인스턴스 속성인 this.state = {filterText: '', inStockOnly: false} 를 FilterableProductTable의 constructor에 추가하여 애플리케이션의 초기 상태를 반영하자.

  • 이 후에 filterText와 inStockOnly를 ProductTable와 SearchBar에 prop으로 전달한다.

  • 마지막으로 이 props를 사용하여 ProductTable의 행을 정렬하고 SearchBar의 폼 필드 값을 설정하자.

  • 이제 애플리케이션의 동작을 볼 수 있다.
  • filterText를 "ball"로 설정하고 앱을 새로고침 해보자.
  • 데이터 테이블이 올바르게 업데이트 된 것을 볼 수 있다.

5단계: 역방향 데이터 흐름 추가하기

  • 지금까지 우리는 계층 구조 아래로 흐르는 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를 가지고 애플리케이션과 컴포넌트를 만드는 데에 대한 사고방식을 얻어갈 수 있기를 바란다.
  • 이전보다 더 많은 타이핑을 해야 할 수 있지만, 코드를 쓸 일보다 읽을 일이 더 많다는 사실을 기억하자.
  • 모듈화되고 명시적인 코드는 읽을 때 더 쉽다.
  • 큰 컴포넌트 라이브러리를 만들게 되면 이 명시성과 모듈성에 감사할 것이며 코드 재사용성을 통해 코드 라인이 줄어들기 시작할 것이다.

출처

리액트 공식문서

좋은 웹페이지 즐겨찾기