React can change how you think about the designs you look at and the apps you build. When you build a user interface with React, you will first break it apart into pieces called components. Then, you will describe the different visual states for each of your components. Finally, you will connect your components together so that the data flows through them. In this tutorial, we’ll guide you through the thought process of building a searchable product data table with React.

React는 디자인을 바라보는 방식앱을 빌드하는 방식을 바꿀 수 있습니다. React로 사용자 인터페이스를 빌드할 때는 먼저 컴포넌트라고하는 조각으로 분해합니다. 그런 다음 각 컴포넌트에 대해 서로 다른 시각적 상태를 설명합니다. 마지막으로 컴포넌트를 서로 연결해 데이터가 흐르도록 합니다. 이 튜토리얼에서는 React로 검색 가능한 데이터 테이블을 구축하는 사고 과정을 안내합니다.

Start with the mockup

JSON API와 디자이너의 목업이 있다고 가정해보자

JSON API는 다음과 같이 일부 데이터를 반환함

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
[
{ "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" }
]

The mockup looks like this:

https://beta.reactjs.org/images/docs/s_thinking-in-react_ui.png

To implement a UI in React, you will usually follow the same five steps.

React에서 UI를 구현하려면 일반적으로 동일한 5단계를 따른다.

1
2
3
4
5
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: Break the UI into a component hierarchy

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

목업의 모든 컴포넌트와 하위 컴포넌트 주위에 상자를 그리고 이름을 지정하기. 디자이너와 같이 일한다면 컴포넌트의 이름이 이미 있을 수도 있다.

여러가지 방식으로 컴포넌트를 분할하는 것을 고려할 수 있는데:

  • Programming - 새로운 함수나 객체를 생성할 때에도 동일한 기법 사용하기.
  • CSS - 클래스 선택자를 어디에 사용할 지 생각하기
  • Design - 디자인의 레이어를 어떻게 구성할지 생각하기

JSON이 잘 구조화되어 있으면 UI 컴포넌트 구조에 자연스럽게 매핑되는 것을 발견할 수 있다. 동일한 형태를 가지고 있는 경우가 많기 때문.

UI를 컴포넌트로 분리하고 각 컴포넌트가 데이터 모델의 한 부분과 일치하도록 해보자

https://beta.reactjs.org/images/docs/s_thinking-in-react_ui_outline.png

  • FilterableProductTable 은 전체 app을 포함
  • SearchBar 는 user input을 받음
  • ProductTable 는 사용자 입력에 따라 리스트를 표시하고 필터링 함
  • ProductCategoryRow 는 각 카테고리에 대한 제목을 표시
  • ProductRow 는 각 제품에 대한 행을 표시

ProductTable을 보면 Name과 Price를 포함하고 있는 헤더가 자체 구성 요소가 아님을 알 수 있다. 👉🏻 선호도의 문제

  • ProductTable 의 목록 안에 표시되므로 ProductTable 의 일부
  • 헤더가 복잡해지는 경우에는 컴포넌트로 따로 분리할 수도 있다.

mockup in the hierarchy:

1
2
3
4
5
FilterableProductTable
SearchBar
ProductTable
ProductCategoryRow
ProductRow

Step 2: Build a static version in React

React에서 정적 버전 빌드하기

정적 버전을 빌드하려면 다른 컴포넌트를 재사용하고 props를 사용하여 데이터를 전달하는 컴포넌트를 빌드하는 것이 좋다.

  • props는 부모에서 자식으로 데이터를 전달하는 방법

계층 구조에서 상위 컴포넌트로부터 빌드하는 “top down” 빌드 또는 하위 컴포넌트부터 작업하는 “bottom up” 중 하나를 선택할 수 있다.

  • 간단한 예제에서는 하향식으로 작성하는 것이 더 쉽고, 대규모 프로젝트에서는 상향식으로 작성하는 것이 쉽다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
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.category} />
);

lastCategory = product.category;
});

return (
<table>
<thead>
<tr>
<th>Name</th>
<th>Price</th>
</tr>
</thead>
<tbody>{rows}</tbody> // rows 배열을 전달
</table>
);
};

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


// 가장 상위 컴포넌트에서부터 products 객체를 받아야 한다.
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 />;
}

https://codesandbox.io/s/0jfw74?file=/App.js&utm_medium=sandpack

컴포넌트를 빌드하고 나면 데이터 모델을 렌더링하는 재사용 가능한 컴포넌트 라이브러리를 갖게 된다. 이 앱은 정적 앱이므로 컴포넌트는 JSX만 반환함.

계층 구조의 맨 위에 있는 컴포넌트(FilterableProductTable)는 데이터 모델을 소품으로 사용.

데이터가 최 상위 컴포넌트에서 트리 하단에 있는 컴포넌트로 흘러내림 👉🏻 단방향 데이터 흐름

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

최소한이지만 UI 상태 표현 완성하기

UI 대화형으로 만들려면 사용자가 기본 데이터 모델을 변경할 수 있도록 해야함 👉🏻 이를 위해 state를 사용

  • 상태(state): 앱이 기억해야하는 최소한의 변경 데이터 집합
  • 상태를 구조화할 때 가장 중요한 원칙
    : 반복하지 않는 것

애플리케이션에 필요한 최소한의 상태 표현을 파악하고, 그 외의 모든 것들은 요청 시에 계산한다.

예를 들어 쇼핑 목록을 만드는 경우,

  • 항목 👉🏻 상태 배열로 저장
  • 목록에 있는 항목의 개수를 표시하려면 다른 상태 값으로 저장하지 말고 배열의 길이를 읽으면 된다.

예제 애플리케이션의 모든 데이터 조각들을 생각해보기

  1. 원래 제품 목록
  2. 사용자가 입력한 검색 텍스트
  3. 체크박스의 값
  4. 필터링된 제품 목록

여기서 상태는 무엇인가? 상태가 아닌 것들을 판별해보자.

  • 시간이 지나도 변하지 않는가? 👉🏻 상태 X
  • 부모로부터 props를 통해 전달받는가? 👉🏻 상태 X
  • 컴포넌트의 기존 state나 props를 기반으로 계산할 수 있는가? 👉🏻 상태 X

이 외의 나머지는 모두 상태이다.

예제 데이터를 보면서 살펴보자.

  1. 원래 제품 목록: props로 전달되었기 때문에 상태 X
  2. 사용자가 입력한 검색 텍스트: 시간이 지남에 따라 변경되고 아무것도 계산할 수 없으므로 상태 O
  3. 체크박스의 값: 시간이 지남에 따라 변경되고 계산이 어려우므로 상태 O
  4. 필터링된 제품 목록: 원래 제품 목록을 가져와서 검색 텍스트 및 확인란 값에 따라 필터링하여 계산할 수 있으므로 상태 X

Step 4: Identify where your state should live

상태가 어디서 살아있어야하는지 식별하기

앱의 최소 상태 데이터를 식별했다면 이제는 상태를 소유하는 컴포넌트를 식별해야한다.

리액트는 단방향 데이터 흐름을 사용하며, 부모 컴포넌트가 자식 컴포넌트로 컴포넌트 계층 구조를 따라 데이터를 전달한다는 것을 기억하자.

  1. 해당 상태를 기반으로 무언갈르 렌더링하는 모든 컴포넌트를 식별한다.
  2. 가장 가까운 공통 상위 컴포넌트, 즉 계층 구조에서 모든 컴포넌트 위에 있는 컴포넌트를 찾는다.
  3. 상태가 어디에 위치해야할 지 결정한다.
    1. 종종 상태를 공통 부모에 직접 넣을 수 있다.
    2. 상태를 공통 부모 위에 있는 컴포넌트에 넣을 수 있다.
    3. 상태를 소유하기에 적합한 컴포넌트를 찾을 수 없는 경우 상태를 보유하기 위한 컴포넌트를 새로 만들어 공통 부모 컴포넌트 위에 추가한다.

우리는 이전 단계에서 검색 입력 텍스트와 체크박스 확인 값이라는 두 가지 상태를 찾아놓음.

이 상태에 대한 전략을 실행해보자.

  1. 상태를 사용하는 컴포넌트 식별하기:
    1. ProductTable은 해당 상태를 기준으로 제품 목록을 필터링해야한다.
    2. SearchBar는 해당 상태를 표시해야 한다.
  2. 공통 부모 찾기: 두 컴포넌트가 공유하는 첫 번째 상위 컴포넌트는 FilterableProductTable
  3. 상태를 어디에 있게할지 결정하기: 필터 텍스트와 체크된 상태 값은 FilterableProductTable에 있어야 한다.

따라서 상태 값은 FilterableProductTable에 저장한다.

useState() Hook으로 컴포넌트에 state를 추가하기

FilterableProductTable의 상단에 상태 변수 두 개를 추가하고 애플리케이션의 초기 상태를 지정:

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

filterText와 inStockOnly를 ProductTable과 SearchBar 컴포넌트에 props로 전달하기

1
2
3
4
5
6
7
8
<div>
<SearchBar filterText={filterText} inStockOnly={inStockOnly} />
<ProductTable
products={products}
filterText={filterText}
inStockOnly={inStockOnly}
/>
</div>

이제 애플리케이션이 어떻게 동작하는지 확인할 수 있다.

https://codesandbox.io/s/n3gc1j?file=/App.js&utm_medium=sandpack

폼을 편집하는 것은 아직 동작하지 않음. 그 이유는?

👉🏻 onChange 핸들러가 없는 양식 필드에 Value 프로퍼티를 제공. 이렇게 되면 읽기 전용 필드가 렌더링된다.

ProductTable과 SearchBar는 filterText 및 inStockOnly prop을 읽어 테이블, input, 체크박스를 렌더링한다. 예를 들어, SearchBar가 입력 값을 채우는 방식은 다음과 같다.

1
2
3
4
function SearchBar( { filterText, inStockOnly }) {
return (
<form>
<input type="text" **value={filterText}** placeholder="Search...">

Step 5: Add inverse data flow

역데이터 흐름 추가하기

현재 애플리케이션은 프로퍼티와 상태가 계층 구조 아래로 흐르면서 올바르게 렌더링된다.

그러나 사용자 입력에 따라 상태를 변경하려면 👉🏻 다른 방향으로 데이터가 흐르도록 해야 한다
form 컴포넌트가 FilterableProductTable 상태를 업데이트 해야한다.

위 예시에서 입력하거나 확인란을 선택하려고 하면 리액트가 입력을 의도적으로 무시한다.

<input value={filterText} />를 작성함으로써, 입력 값 prop이 항상 FilterableProductTable에서 전달된 filterText 상태와 같도록 만들어 놓았기 때문.

즉, filterText 상태가 설정되지 않으므로 입력은 변경되지 않는다.

input이 변경될 때마다 변경 사항을 반영해서 상태가 업데이트 되도록 만들고 싶다면

  • 상태는 FilterableProductTable가 가지고 있으므로 FilterableProductTable 함수만이 setFilterText와 setInStockOnly 를 호출할 수 있음

SearchBar가 FilterableProductTable의 상태를 업데이트하도록 하려면

  • setFilterText와 같은 함수를 SearchBar에 전달해야한다.
1
2
3
4
5
6
7
8
9
10
11
function FilterableProductTable({ products }) {
const [filterText, setFilterText] = useState('');
const [inStockOnly, setInStockOnly] = useState(false);

return (
<div>
<SearchBar
filterText={filterText}
inStockOnly={inStockOnly}
onFilterTextChange={setFilterText}
onInStockOnlyChange={setInStockOnly} />

SearchBar 내부에는 onChange 이벤트 핸들러를 추가하고 이 핸들러에서 상위 상태를 설정한다.

1
2
3
4
5
6
<input
type="text"
value={filterText}
placeholder="Search..."
onChange={(e) => onFilterTextChange(e.target.value)}
/>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
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} />;
}

단일 책임 원칙(single responsibility principle)

  • 객체 지향 프로그래밍에서 중요한 원칙 중 하나
  • ‘클래스나 모듈은 하나의 액터에게만 책임을 져야한다.’는 컴퓨터 프로그래밍 원칙
  • 액터라는 용어는 모듈을 변경해야하는 그룹을 의미 (한 명 이상의 이해관계자 또는 사용자로 구성됨)
    • The term actor refers to a group (consisting of one or more stakeholders or users) that requires a change in the module.
  • 로버트 C. 마틴은 이 원칙을 ‘클래스를 변경할 이유가 하나만 있어야 한다.’라고 표현
  • “원인(이유)”이라는 단어 때문에 혼란이 생겨 그는 “원칙은 사람에 관한 것이다”라고 명확히 함.
  • 주문 클래스 👉🏻 주문을 처리하는 책임만 가지고 있어야 한다.
    • 결제나 배송과 같은 책임은 다른 클래스에서 처리되어야 함
    • 이렇게 되면 주문 클래스는 주문과 관련된 기능만을 가지고 있고 코드의 복잡도가 감소하여 유지보수가 쉬워진다.
  • 단일 책임 원칙을 지키면 클래스 간 의존성이 감소하고, 결합도가 낮아져 시스템의 유연성과 확장성이 향상된다.

History

  • 로버트 C. 마틴이 객체 지향 설계의 원칙의 일부인 “OOD의 원칙”이라는 글에서 소개

Props vs State

  • Props는 함수에 전달하는 arguments와 같다.
    • 부모 컴포넌트가 자식 컴포넌트에 데이터를 전달
    • ex) form은 버튼에 색상 프로퍼티를 전달할 수 있다.
  • State는 컴포넌트의 메모리와 같다.
    • 컴포넌트가 일부 정보를 추적하고 상호작용에 반응하여 변경할 수있게 해줌
    • ex) 버튼은 isHovered 상태를 추적할 수 있다.

props와 state는 다르지만 함께 작동한다. 부모 컴포넌트는 일부 정보를 state에 보관하고 이를 자식 컴포넌트에 프로퍼티로 전달하는 경우가 있다.