Skip to content

Latest commit

 

History

History
530 lines (425 loc) · 19.6 KB

File metadata and controls

530 lines (425 loc) · 19.6 KB

React Component

REST API와 GraphQL

백엔드에서 JSON 형태의 데이터를 돌려주는 API를 제공한다고 가정한다면 대부분은 REST API 또는 GraphQL로 API를 제공합니다. 프론트엔드에서는 이 데이터를 사용자가 볼 수 있도록 UI를 구성합니다. React에서는 선언형(HTMl과 유사한 모양의 DSL)으로 UI를 구성할 수 있습니다.

REST API란 무엇인가?

REST API의 REST는 Representational State Transfer의 약자로 REST API는 웹에서 사용되는 데이터나 자원(Resource)을 HTTP URI로 표현하고, HTTP 프로토콜을 통해 요청과 응답을 정의하는 방식을 의미합니다.

  • CRUD(Create, Read, Update, Delete)와 HTTP 메서드 적용 규칙
    • GET 메서드는 **데이터를 조회(READ)**하기 위해서 사용합니다. GET 메서드를 사용하여 요청을 보내고, 이때 GET 메서드는 body를 가지지 않기 때문에 query parameter를 사용하여 필요한 리소스를 전달합니다. GET메서드는 서버의 데이터를 변화시키지 않는 요청에 사용해야 합니다.
    • POST 메서드는 **데이터를 추가(CREATE)**하기 위해서 사용합니다. POST메서드는 요청마다 새로운 리소스를 생성하고 PUT 메서드는 요청마다 같은 리소스를 반환합니다. 이렇게 매 요청마다 같은 리소스를 반환하는 특징을 멱등 하다고 합니다. 그렇기 때문에 멱등성을 가지는 PUT메서드와 그렇지 않은 POST메서드를 구분하여 사용해야 합니다.
    • PUT 메서드와 PATCH메서드는 **데이터를 업데이트(UPDATE)**하기 위해 사용합니다. PUT 메서드의 경우 리소스의 모든 것을 업데이트하고 PATCH 메서드는 리소스의 일부를 업데이트합니다. 따라서 PUT 메서드는 교체, PATCH메서드는 수정의 용도로 사용합니다.

REST API의 한계

  1. Overfetch: 필요 없는 데이터까지 제공함
    아래 이미지에서처럼 유저의 이름만 필요한 상황에서 REST API를 사용한다면, 응답 데이터에는 유저의 주소, 생일 등과 같이 실제로는 클라이언트에게 필요없는 정보가 포함돼 있을 수 있습니다.

  2. Underfetch: endpoint가 필요한 정보를 충분히 제공하지 못함
    아래 이미지에서처럼 REST API에서는 각각의 자원에 따라 엔드포인트를 구분하기 때문에 클라이언트는 필요한 정보를 모두 확보하기 위해서 추가적인 요청을 보내야만 합니다.

GraphQL은 왜 등장했는가?

GraphQL은 Graph + Query Language의 줄임말로 API를 위한 쿼리 언어입니다.

그래프()

그래프(Graph)는 여러 개의 점들이 서로 복잡하게 연결되어 있는 관계를 표현한 자료구조를 뜻합니다. 하나의 점을 그래프에서는 Node 또는 **정점(vertex)**라고 표현하고, 하나의 선은 간선(edge) 이라고 합니다. 직접적인 관계가 있는 경우 두 점 사이를 이어주는 선이 있으며, 간접적인 관계라면 몇 개의 점과 선에 걸쳐 이어집니다.

GraphQL에서는 모든 데이터가 그래프 형태로 연결돼 있다고 전제합니다. 일대일로 연결된 관계도, 여러 계층으로 이루어진 관계도 모두 그래프입니다. 트리나 그래프나 노드와 노드를 연결하는 간선으로 구성된 자료구조이기 때문입니다. 단지 그 그래프를 누구의 입장에서 정렬하느냐(클라이언트가 어떤 데이터를 필요로 하느냐)에 따라 트리 구조를 이룰 수 있습니다.

이를 통해 GraphQL은 클라이언트 요청에 따라 유연하게 트리 구조의 JSON 데이터를 응답으로 전송할 수 있습니다. 다시 말해 GraphQL은 REST API 방식의 고정된 자원이 아닌 클라이언트 요청에 따라 유연하게 자원을 가져올 수 있다는 점에서 엄청난 이점을 갖습니다.

GraphQL의 장점

  1. 하나의 endpoint 요청
    ****하나의 endpoint로 요청을 받고 그 요청에 따라 query, mutaion을 resolver 함수로 전달해서 요청에 응답합니다. 모든 클라이언트 요청은 POST 메소드를 사용합니다.

  2. NO! under & overfetching
    ****여러 개의 endpoint 요청을 할 필요없이 하나의 endpoint에서 쿼리를 이용해 원하는 데이터를 정확하게 API에 요청하고 응답으로 받을 수 있습니다.

REST API vs GraphQL

  • REST API는 Resource에 대한 형태 정의와 데이터 요청 방법이 연결되어 있지만, GraphQL에서는 Resource에 대한 형태 정의와 데이터 요청이 완전히 분리되어 있습니다.\
  • REST API는 Resource의 크기와 형태를 서버에서 결정하지만, GraphQL에서는 Resource에 대한 정보만 정의하고, 필요한 크기와 형태는 클라이언트 단에서 요청 시 결정합니다.\
  • REST API는 URI가 Resource를 나타내고 Method가 작업의 유형을 나타내지만, GraphQL에서는 GraphQL Schema가 Resource를 나타내고 Query, Mutation 타입이 작업의 유형을 나타냅니다.\
  • REST API는 여러 Resource에 접근하고자 할 때 여러 번의 요청이 필요하지만, GraphQL에서는 한번의 요청에서 여러 Resource에 접근할 수 있습니다.\
  • REST API에서 각 요청은 해당 엔드포인트에 정의된 핸들링 함수를 호출하여 작업을 처리하지만, GraphQL에서는 요청 받은 각 필드에 대한 resolver를 호출하여 작업을 처리합니다.

JSONJavaScript Object Notation의 약어로 Javascript 객체 문법으로 구조화된 데이터를 표현하기 위한 문자 기반의 표준 포맷입니다. 웹 어플리케이션에서 데이터를 전송할 때 일반적으로 사용합니다.

객체와 문자 사이의 변환

  • parse() : JSON 문자열을 구문 분석하여 문자열이 설명하는 JavaScript 값 또는 객체를 구성합니다.
  • stringify(): 자바스크립트 값을 JSON 문자열로 변환합니다.

명령형 프로그램은 알고리즘(How)을 명시하고 목표는 명시하지 않는 데 반해 선언형 프로그램은 목표(What)를 명시하고 알고리즘을 명시하지 않습니다.

React component와 props

React의 강력한 특징들 중 하나는 다음과 같습니다.\

  • "Component-Based"
  • “Build encapsulated components that manage their own state, then compose them to make complex UIs.”

이렇게 React에서는 캡슐화된 component들을 만들어서 복잡한 UI를 만들 수 있습니다.\

캡슐화된 컴포넌트 만드는 기준

  • SRP(Single Responsibility Principle)

  • CSS에서 사용하는 class를 기준으로 캡슐화할 수 있습니다.

    <div class="product">
      <div class="thumbnail">
        ....
        <div class="price">....</div>
      </div>
    </div>
  • Design's Layer 만약 디자이너와 함께 작업한다면 이미 컴포넌트 이름이 지정된 경우가 많습니다.

  • Information Architecture JSON schema에 따라 캠슐화할 수 있고 실제 작업에서 많이 사용합니다.

  • Atomic Design 우리가 잘 알고 있는 계층형 구조를 몇 가지 카테고리로 묶는 방법입니다.

Thinking in React

Step 1: Break the UI into a component hierarchy

UI를 컴포넌트의 계층 구조로 쪼개기

Step 2 : Build a static version in React

정적인 버전으로 만들기

Thinking in ReactStart with the mockup에서는 다음과 같은 mockup과 JSON API가 나옵니다.

  • mockup mockup
  • JSON API
[
  { "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" }
]

mockup

  1. mockup의 모든 컴포넌트와 하위 컴포넌트 주위에 상자를 그리고 이름을 지정합니다.\
type Product = {
  category: string;
  price: string;
  stocked: boolean;
  name: string;
};

const products: Product[] = [
  { 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() {
  const categories = products.reduce(
    (acc: string[], product: Product) =>
      acc.includes(product.category) ? acc : [...acc, product.category],
    []
  );

  return (
    <div className="filterable-product-table">
      <div className="search-bar">
        <div>
          <input type="text" placeholder="Search..." />
        </div>
        <div>
          <input type="checkbox" id="only-stock" />
          <label htmlFor="only-stock">Only show products in stock</label>
        </div>
      </div>
      <table className="product-table">
        <thead>
          <tr>
            <th>Name</th>
            <th>Price</th>
          </tr>
        </thead>
        <tbody>
          <tr>
            <th colSpan={2}>{categories[0]}</th>
          </tr>
          {products
            .filter((product) => product.category === categories[0])
            .map((product) => (
              <tr key={product.name}>
                <td>{product.name}</td>
                <td>{product.price}</td>
              </tr>
            ))}
          <tr>
            <th colSpan={2}>{categories[1]}</th>
          </tr>
          {products
            .filter((product) => product.category === categories[1])
            .map((product) => (
              <tr key={product.name}>
                <td>{product.name}</td>
                <td>{product.price}</td>
              </tr>
            ))}
        </tbody>
      </table>
    </div>
  );
}

1-1.Extract Function 일단 길게 코드를 작성하고, 적절히 자를 수 있는 부분이 보일 때 함수로 추출합니다.

  • App.tsx
import ProductsInCategory from './component/ProductsInCategory';
// import type은 한 칸 아래로 띄어줘서 보기 쉽게 관리합니다.
import type Product from './types/Product';

const products: Product[] = [
  { 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() {
  const categories = products.reduce(
    (acc: string[], product: Product) =>
      acc.includes(product.category) ? acc : [...acc, product.category],
    []
  );

  return (
    <div className="filterable-product-table">
      <div className="search-bar">
        <div>
          <input type="text" placeholder="Search..." />
        </div>
        <div>
          <input type="checkbox" id="only-stock" />
          <label htmlFor="only-stock">Only show products in stock</label>
        </div>
      </div>
      <table className="product-table">
        <thead>
          <tr>
            <th>Name</th>
            <th>Price</th>
          </tr>
        </thead>
        <tbody>
          {categories.map((category) => (
            <ProductsInCategory
              key={category}
              category={category}
              products={products}
            />
          ))}
        </tbody>
      </table>
    </div>
  );
}
  • components/ProductsCategory.tsx
import type Product from '../types/Product';

type ProductsInCategoryProps = {
  category: string;
  products: Product[];
};

export default function ProductsInCategory({
  category,
  products,
}: ProductsInCategoryProps) {
  const productInCategory = products.filter(
    (product) => product.category === category
  );
  return (
    <>
      <tr>
        <th colSpan={2}>{category}</th>
      </tr>
      {productInCategory.map((product) => (
        <tr key={product.name}>
          <td>{product.name}</td>
          <td>{product.price}</td>
        </tr>
      ))}
    </>
  );
}

1-2. Design’s Layer JSON이 잘 구조화되어 있으면 UI의 컴포넌트 구조에 자연스럽게 매핑되는 것을 종종 발견할 수 있습니다. 이는 UI와 데이터 모델이 동일한 정보 아키텍처, 즉 동일한 형태를 가지고 있는 경우가 많기 때문입니다. 따라서 UI를 컴포넌트로 분리하면 각 컴포넌트가 데이터 모델의 한 부분과 일치하다는 것을 알 수 있습니다.

  • App.tsx
import FilterableProductTable from './component/FilterableProductTable';

import type Product from './types/Product';

const products: Product[] = [
  { 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} />;
}
  • components/FilterableProductTable.tsx
import ProductTable from './ProductTable';
import SearchBar from './SearchBar';

import type Product from '../types/Product';

type FilterableProductTableProps = {
  products: Product[];
};

export default function FilterableProductTable({
  products,
}: FilterableProductTableProps) {
  return (
    <div className="filterable-product-table">
      <SearchBar />
      <ProductTable products={products} />
    </div>
  );
}
  • components/SearchBar.tsx
export default function SearchBar() {
  return (
    <div className="search-bar">
      <div>
        <input type="text" placeholder="Search..." />
      </div>
      <div>
        <CheckBoxField label="Only show products in stock" />
      </div>
    </div>
  );
}
  • components/CheckBoxField.tsx
export default function CheckBoxField({ label }: { label: string }) {
  const id = useRef(`checkbox-${label}`.replace(/ /g, '-').toLowerCase());
  return (
    <>
      <input type="checkbox" id={id.current} />
      <label htmlFor={id.current}>{label}</label>
    </>
  );
}
  • components/ProductTable.tsx Inline Function을 사용할 수 있습니다.
import ProductsInCategory from './ProductsInCategory';

import selectCategories from '../utils/selectCategories';

import type Product from '../types/Product';

type ProductTableProps = {
  products: Product[];
};

export default function ProductTable({ products }: ProductTableProps) {
  const categories = selectCategories(products);
  return (
    <table className="product-table">
      <thead>
        <tr>
          <th>Name</th>
          <th>Price</th>
        </tr>
      </thead>
      <tbody>
        {categories.map((category) => (
          <ProductsInCategory
            key={category}
            category={category}
            products={products}
          />
        ))}
      </tbody>
    </table>
  );
}
  • utils/selectCategories.ts
import type Product from '../types/Product';

export default function selectCategories(products: Product[]): string[] {
  return products.reduce((acc: string[], product: Product) => {
    const { category } = product;
    return acc.includes(category) ? acc : [...acc, category];
  }, []);
}
  • component/ProductsInCategory.tsx Inline Function을 사용할 수 있습니다.
import ProductCategoryRow from './ProductCategoryRow';
import ProductRow from './ProductRow';
import selectProducts from '../utils/selectProducts';

import type Product from '../types/Product';

type ProductsInCategoryProps = {
  category: string;
  products: Product[];
};

export default function ProductsInCategory({
  category,
  products,
}: ProductsInCategoryProps) {
  const productInCategory = selectProducts(products, category);
  return (
    <>
      <ProductCategoryRow category={category} />
      {productInCategory.map((product) => (
        <ProductRow key={product.name} product={product} />
      ))}
    </>
  );
}
  • utils/selectProducts.ts
import type Product from '../types/Product';

export default function selectProducts(
  items: Product[],
  category: string
): Product[] {
  return items.filter((item) => item.category === category);
}
  • component/ProductCategoryRow.tsx
export default function ProductCategoryRow({ category }: { category: string }) {
  return (
    <tr>
      <th colSpan={2}>{category}</th>
    </tr>
  );
}
  • component/ProductRow.tsx
import type Product from '../types/Product';

type ProductRowProps = {
  product: Product;
};

export default function ProductRow({ product }: ProductRowProps) {
  return (
    <tr>
      <th>{product.name}</th>
      <th>{product.price}</th>
    </tr>
  );
}

참고 자료