게으른 컴공생
article thumbnail

React 빠르게 복습하기 2. Thinking in React

개요

이번엔 공식홈페이지의 두번째 자료를 따라가보도록 하겠다

 

Thinking in React – React

The library for web and native user interfaces

react.dev

 

 

React에서 UI를 만드려면

1. component로 구분하고

2. state에 따른 component의 생김새를 만들어주고

3. component 간의 data flow를 만들어 주면 된다고 한다.

공식 홈페이지에 있는 검색가능한 테이블 앱을 같이 만들며 이해해보자.

 

우선 데이터와 디자인은 정해져있고 다음과 같다.(API 명세와 design mock up)

 

보통 React로 UI를 구성할 때 아래 5가지 step을 따른다고 한다.

Step 1: Break the UI into a component hierarchy

Step 2: Build a static version in React

Step 3: Find the minimal but complete representation of UI state

Step 4: Identify where your state should live

Step 5: Add inverse data flow

 

Step 1: UI를 component 계층으로 분석하기

공식홈페이지에서의 분류

component를 나누는 것은 그냥 평소에 객체 디자인할 때 하던대로 하면 된다.

single principle rule을 고려해도 되고.... 뭐 기본적으로 하던대로 상식적으로 ㅇㅇ

 

공식 홈페이지에서는 위와 같이 나눠놨다.

1 -> 전체 앱 wrapping

2 -> user가 상호작용 가능

3 -> 상호작용 결과 표시

4 -> 상호작용 결과 중 header

5 -> 상호작용 결과 중 body

정도로 구분했다.

 

그리고 계층 구조는

1 밑에

 2와

 3이 있고 얘 밑에는

   4와

   5가 있음

라고 생각하면 되겠다.

 

Step 2: 상태를 고려하지 않은 정적인 상태 구현

ㅋㅋㅋㅋㅋㅋ

이제 다 나눴으니 실제로 구현을 하면 된다.

구현은 1. 정적인 상태 구현 2. 동적인 상태 구현 순으로 진행하는 것이 좋다고 한다.

정적인 상태를 구현할 때에는 state가 아니라 prop을 쓰면 된다.

state는 오직 동적인 상태를 위한 개념이므로..

 

복잡한 앱은 bottom-up으로 쉬운 앱은 top-down으로 접근하는 걸 추천한다고 한다.

 

뭐 일단 구현을 해보면 다음과 같다.

 

function ProductCategoryRow({ category }) {
  return (
    <tr>
      <th colSpan="2">
        {category}
      </th>
    </tr>
  );
}

function ProductRow({ product }) {
  const name = product.stocked ? product.name :
    <span style={{ color: 'red' }}>
      {product.name}
    </span>;

  return (
    <tr>
      <td>{name}</td>
      <td>{product.price}</td>
    </tr>
  );
}

function ProductTable({ products }) {
  const rows = [];
  let lastCategory = null;

  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>
  );
}

function SearchBar() {
  return (
    <form>
      <input type="text" placeholder="Search..." />
      <label>
        <input type="checkbox" />
        {' '}
        Only show products in stock
      </label>
    </form>
  );
}

function FilterableProductTable({ products }) {
  return (
    <div>
      <SearchBar />
      <ProductTable products={products} />
    </div>
  );
}

const PRODUCTS = [
  {category: "Fruits", price: "$1", stocked: true, name: "Apple"},
  {category: "Fruits", price: "$1", stocked: true, name: "Dragonfruit"},
  {category: "Fruits", price: "$2", stocked: false, name: "Passionfruit"},
  {category: "Vegetables", price: "$2", stocked: true, name: "Spinach"},
  {category: "Vegetables", price: "$4", stocked: false, name: "Pumpkin"},
  {category: "Vegetables", price: "$1", stocked: true, name: "Peas"}
];

export default function App() {
  return <FilterableProductTable products={PRODUCTS} />;
}

 

구체적으로 짚고 갈만한 문법은 다 javascript 문법인데,

1. ? : 연산자와(이거 이름이 뭐지)

2. !== 연산자이다(이것도 이름을 모르겠네)

 

1번은 ? 앞 식이 참이냐 거짓이냐에 따라 참이면 : 앞의 식을 거짓이면 : 뒤의 식을 고르는 것이라는 건 대부분 알고 있을 것 같다.

다만 React에서는 상태에 따라 Component를 그리냐 마냐에 이용해서 자주 쓴다고 한다.

지금 예제는 상태에 따라 다르게 그리는 것이지만 : 앞이나 뒤에 아무것도 안넣으면 그렇게도 할 수 있겠다.

https://react.dev/learn#conditional-rendering

 

2번은 이제 오랜만에 봐서 헷갈렸는데 형변환해서 값을 비교하지 않고 자료형부터 비교해서 자료형이 다르면 false를 뱉는 비교연산자이다.

https://miiingo.tistory.com/337

 

Step 3: state 정의하기

그럼 이제 데이터들 중에 어떤 것이 state인지 판단해야 한다.

지금 현재 식별되는 데이터는 다음과 같다.

 

1. product list

2. user가 입력한 search text

3. checkbox의 값(true/false)

4. 결과로 나오는(filtering된) product list

 

공식 홈페이지에 따르면 이 데이터가 state인지 판단하기 위해 다음 3가지를 생각해봐야한다.

  • 시간이 지나면서 변하는 값인가? 아니라면 state가 아니다.
  • parent가 prop으로 넘겨주는 값인가? 그렇다면 state가 아니다.
  • 다른 state를 가지고 계산해낼 수 있는 값인가? 그렇다면 state가 아니다.

 

1. prop으로 전달되므로 state가 아니다.

2. 시간이 지나면서 변하고 다른 값들로 계산해낼 수 없으니 state이다.

3. 시간이 지나면서 변하고 다른 값들로 계산해낼 수 없으니 state이다.

4. 1번을 2, 3번을 이용해서 filtering 하면 얻어지는 데이터이므로 state가 아니다

 

Step 4: state가 있어야할 위치 결정하기

공식 홈페이지에 따르면 다음을 생각해보면 state가 위치할 component를 결정할 수 있다고 한다.

 

  • state를 사용하는 component 찾기
  • 그 component들의 공통 parent 찾기
  • state를 넣을 곳 결정하기
    1. 공통 parent component에 넣기
    2. 공통 parent component의 위에 있는 component에 넣기(parent말고 뭐 grandparent 정도?)
    3. 어디 넣을 지 모르겠다면 공통 parent의 위에 state를 가지는 역할의 component 만들어서 거기 넣기

현재 예제에서는 두 개의 state가 있다.

1. user가 입력한 search text

2. checkbox의 값

 

1을 필요로 하는 component는 그림에서 3번이었던 ProductTable이고

2를 필요로 하는 component는 그림에서 2번이었던 SearchBar이다.

이 둘의 공통 조상은 그림에서 1번이었던 FilterableProductTable이고

그래서 우리는 현재 식별된 2개의 state를 FilterableProductTable component에 넣을 것이다.

그리고 각 state를 필요로하는 ProductTable과 SearchBar에게 prop의 형태로 넘겨줄 것이다.

 

import { useState } from 'react';

function FilterableProductTable({ products }) {
  const [filterText, setFilterText] = useState('');
  const [inStockOnly, setInStockOnly] = useState(false);

  return (
    <div>
      <SearchBar 
        filterText={filterText} 
        inStockOnly={inStockOnly} />
      <ProductTable 
        products={products}
        filterText={filterText}
        inStockOnly={inStockOnly} />
    </div>
  );
}

function ProductCategoryRow({ category }) {
  return (
    <tr>
      <th colSpan="2">
        {category}
      </th>
    </tr>
  );
}

function ProductRow({ product }) {
  const name = product.stocked ? product.name :
    <span style={{ color: 'red' }}>
      {product.name}
    </span>;

  return (
    <tr>
      <td>{name}</td>
      <td>{product.price}</td>
    </tr>
  );
}

function ProductTable({ products, filterText, inStockOnly }) {
  const rows = [];
  let lastCategory = null;

  products.forEach((product) => {
    if (
      product.name.toLowerCase().indexOf(
        filterText.toLowerCase()
      ) === -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>
  );
}

function SearchBar({ filterText, inStockOnly }) {
  return (
    <form>
      <input 
        type="text" 
        value={filterText} 
        placeholder="Search..."/>
      <label>
        <input 
          type="checkbox" 
          checked={inStockOnly} />
        {' '}
        Only show products in stock
      </label>
    </form>
  );
}

const PRODUCTS = [
  {category: "Fruits", price: "$1", stocked: true, name: "Apple"},
  {category: "Fruits", price: "$1", stocked: true, name: "Dragonfruit"},
  {category: "Fruits", price: "$2", stocked: false, name: "Passionfruit"},
  {category: "Vegetables", price: "$2", stocked: true, name: "Spinach"},
  {category: "Vegetables", price: "$4", stocked: false, name: "Pumpkin"},
  {category: "Vegetables", price: "$1", stocked: true, name: "Peas"}
];

export default function App() {
  return <FilterableProductTable products={PRODUCTS} />;
}

 

Step 5: state 변화를 위해 윗 방향으로의 data flow 추가

지금은 setFilterText랑 setInStockOnly를 어디에서도 사용하고 있지 않기 때문에

state가 변하고 있지를 않다.

이를 SearchBar에 추가해줘야 하고 그러면 data의 흐름이

1. SearchBar에서 text입력이나 checkbox상태 변경 -> 2. FilterableProductTable의 state가 변경됨

-> 3. SearchBar와 ProductTable의 값이 변경됨

과 같아진다.

이때 2->3은 그냥 기존의 데이터 흐름이지만

1->2는 데이터의 방향이 반대이다.

아무튼 그걸 추가하면 다음과 같은 코드가 완성된다.

 

import { useState } from 'react';

function FilterableProductTable({ 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>
  );
}

function ProductCategoryRow({ category }) {
  return (
    <tr>
      <th colSpan="2">
        {category}
      </th>
    </tr>
  );
}

function ProductRow({ product }) {
  const name = product.stocked ? product.name :
    <span style={{ color: 'red' }}>
      {product.name}
    </span>;

  return (
    <tr>
      <td>{name}</td>
      <td>{product.price}</td>
    </tr>
  );
}

function ProductTable({ products, filterText, inStockOnly }) {
  const rows = [];
  let lastCategory = null;

  products.forEach((product) => {
    if (
      product.name.toLowerCase().indexOf(
        filterText.toLowerCase()
      ) === -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>
  );
}

function SearchBar({
  filterText,
  inStockOnly,
  onFilterTextChange,
  onInStockOnlyChange
}) {
  return (
    <form>
      <input 
        type="text" 
        value={filterText} placeholder="Search..." 
        onChange={(e) => onFilterTextChange(e.target.value)} />
      <label>
        <input 
          type="checkbox" 
          checked={inStockOnly} 
          onChange={(e) => onInStockOnlyChange(e.target.checked)} />
        {' '}
        Only show products in stock
      </label>
    </form>
  );
}

const PRODUCTS = [
  {category: "Fruits", price: "$1", stocked: true, name: "Apple"},
  {category: "Fruits", price: "$1", stocked: true, name: "Dragonfruit"},
  {category: "Fruits", price: "$2", stocked: false, name: "Passionfruit"},
  {category: "Vegetables", price: "$2", stocked: true, name: "Spinach"},
  {category: "Vegetables", price: "$4", stocked: false, name: "Pumpkin"},
  {category: "Vegetables", price: "$1", stocked: true, name: "Peas"}
];

export default function App() {
  return <FilterableProductTable products={PRODUCTS} />;
}

 

 

마무리

이로써 리액트로 기본적인 앱을 짤 준비가 끝난 것 같다.

학습할 내용이 무궁무진하지만 이 정도까지 하고 바로 방울이 프로젝트의 랜딩페이지를 만들어봐야겠다.

'기술 > 리액트' 카테고리의 다른 글

React 빠르게 복습하기 1. Tutorial  (0) 2023.08.22
profile

게으른 컴공생

@노나니노나

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!

검색 태그