백엔드에서 JSON 형태의 데이터를 돌려주는 API를 제공한다고 가정한다면 대부분은 REST API 또는 GraphQL로 API를 제공합니다. 프론트엔드에서는 이 데이터를 사용자가 볼 수 있도록 UI를 구성합니다. React에서는 선언형(HTMl과 유사한 모양의 DSL)으로 UI를 구성할 수 있습니다.
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메서드는 수정의 용도로 사용합니다.
-
Overfetch: 필요 없는 데이터까지 제공함
아래 이미지에서처럼 유저의 이름만 필요한 상황에서 REST API를 사용한다면, 응답 데이터에는 유저의 주소, 생일 등과 같이 실제로는 클라이언트에게 필요없는 정보가 포함돼 있을 수 있습니다.
-
Underfetch: endpoint가 필요한 정보를 충분히 제공하지 못함
아래 이미지에서처럼 REST API에서는 각각의 자원에 따라 엔드포인트를 구분하기 때문에 클라이언트는 필요한 정보를 모두 확보하기 위해서 추가적인 요청을 보내야만 합니다.
GraphQL은 Graph + Query Language의 줄임말로 API를 위한 쿼리 언어입니다.
그래프()
그래프(Graph)는 여러 개의 점들이 서로 복잡하게 연결되어 있는 관계를 표현한 자료구조를 뜻합니다. 하나의 점을 그래프에서는 Node 또는 **정점(vertex)**라고 표현하고, 하나의 선은 간선(edge) 이라고 합니다. 직접적인 관계가 있는 경우 두 점 사이를 이어주는 선이 있으며, 간접적인 관계라면 몇 개의 점과 선에 걸쳐 이어집니다.
GraphQL에서는 모든 데이터가 그래프 형태로 연결돼 있다고 전제합니다. 일대일로 연결된 관계도, 여러 계층으로 이루어진 관계도 모두 그래프입니다. 트리나 그래프나 노드와 노드를 연결하는 간선으로 구성된 자료구조이기 때문입니다. 단지 그 그래프를 누구의 입장에서 정렬하느냐(클라이언트가 어떤 데이터를 필요로 하느냐)에 따라 트리 구조를 이룰 수 있습니다.
이를 통해 GraphQL은 클라이언트 요청에 따라 유연하게 트리 구조의 JSON 데이터를 응답으로 전송할 수 있습니다. 다시 말해 GraphQL은 REST API 방식의 고정된 자원이 아닌 클라이언트 요청에 따라 유연하게 자원을 가져올 수 있다는 점에서 엄청난 이점을 갖습니다.
-
하나의 endpoint 요청
****하나의 endpoint로 요청을 받고 그 요청에 따라 query, mutaion을 resolver 함수로 전달해서 요청에 응답합니다. 모든 클라이언트 요청은POST메소드를 사용합니다.
-
NO! under & overfetching
****여러 개의 endpoint 요청을 할 필요없이 하나의 endpoint에서 쿼리를 이용해 원하는 데이터를 정확하게 API에 요청하고 응답으로 받을 수 있습니다.
- 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를 호출하여 작업을 처리합니다.
JSON은 JavaScript Object Notation의 약어로 Javascript 객체 문법으로 구조화된 데이터를 표현하기 위한 문자 기반의 표준 포맷입니다. 웹 어플리케이션에서 데이터를 전송할 때 일반적으로 사용합니다.
- parse() : JSON 문자열을 구문 분석하여 문자열이 설명하는 JavaScript 값 또는 객체를 구성합니다.
- stringify(): 자바스크립트 값을 JSON 문자열로 변환합니다.
명령형 프로그램은 알고리즘(How)을 명시하고 목표는 명시하지 않는 데 반해 선언형 프로그램은 목표(What)를 명시하고 알고리즘을 명시하지 않습니다.
React의 강력한 특징들 중 하나는 다음과 같습니다.\
- "Component-Based"
- “Build encapsulated components that manage their own state, then compose them to make complex UIs.”
이렇게 React에서는 캡슐화된 component들을 만들어서 복잡한 UI를 만들 수 있습니다.\
-
CSS에서 사용하는 class를 기준으로 캡슐화할 수 있습니다.
<div class="product"> <div class="thumbnail"> .... <div class="price">....</div> </div> </div>
-
Design's Layer 만약 디자이너와 함께 작업한다면 이미 컴포넌트 이름이 지정된 경우가 많습니다.
-
Information Architecture JSON schema에 따라 캠슐화할 수 있고 실제 작업에서 많이 사용합니다.
-
Atomic Design 우리가 잘 알고 있는 계층형 구조를 몇 가지 카테고리로 묶는 방법입니다.
UI를 컴포넌트의 계층 구조로 쪼개기
정적인 버전으로 만들기
Thinking in React의 Start with the 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의 모든 컴포넌트와 하위 컴포넌트 주위에 상자를 그리고 이름을 지정합니다.\
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>
);
}


.png)
.png)