React로 사고하기 -2

Jetom·2021년 10월 4일
0

개념정리(react)

목록 보기
8/17
post-thumbnail

1편에 이어서 2편! 열심히 또 적어서 정리하기로~!! 🤓


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

UI를 상호작용하게 만드려면 데이터 모델을 변경할 수 있는 방법이 있어야하는데, 이를 React는 state를 통해 변경한다. 애플리케이션에서 필요로 하는 변경 가능한 state의 최소 집합을 생각해야하는데 여기서 핵심은 중복배제원칙(= 중복은 피하라는 말? 같다)이다.

애플리케이션이 필요로 하는 가장 최소한의 state를 찾고 나머지 모든 것들이 필요에 따라 계산되게하자! TO DO List로 예를들면 아이템을 저장하는 배열만 유지하고 아이템 갯수를 표현하는 state를 별도로 만들지않아야한다. TO DO 갯수를 렌더링해야한다면 아이템 배열의 길이를 가져오면 된다.(= 굳이 갯수를 가져오는 state를 만들지 말고 배열의 길이를 가져오라는 말)

예시 애플리케이션 내 데이터들을 생각해보자 다음과 같은 데이터를 가지고있을것이다.

  • 제품의 원본 목록
  • 유저가 입력한 검색어
  • 체크박스의 값
  • 필터링 된 제품들의 목록

각각 어떤것이 state가 되어야 할지 생각해야한다면 아래의 세 가지 질문을 통해 결정할 수 있다.

  1. 부모로부터 props를 통해 전달됩니까? 그러면 확실히 state가 아닙니다.
  2. 시간이 지나도 변하지 않나요? 그러면 확실히 state가 아닙니다.
  3. 컴포넌트 안의 다른 state나 props를 가지고 계산 가능한가요? 그렇다면 state가 아닙니다.

제품의 원본 목록은 props를 통해 전달되므로 state가 아니다. 검색어와 체크박스는 state로 볼 수 있는데 시간이 지남에 따라 볂기도 하면서 다른 것들로부터 계산될 수 없기 때문이다. 그리고 필터링된 목록은 state가 아니다. 제품의 원본 목록과 검색어, 체크박스의 값을 조합해 계산해낼 수 있기 때문이다.

결과적으로 애플리케이션은 다음과 같은 state를 가진다.

  • 유저가 입력한 검색어
  • 체크박스의 값

4단계: 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를 기반으로 렌더링하는 모든 컴포넌트를 찾으세요.
  • 공통 소유 컴포넌트 (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단계: 역방향 데이터 흐름 추가하기


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개국어를 발휘할 수 있을때까지 뽜이팅!!🤨

profile
사람이 좋은 인간 리트리버 신혜리입니다🐶

0개의 댓글