Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

03 WIP react design #6

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion _includes/extensions/theme-toggle.html
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
try {
data = JSON.parse(data ? data : '');
} catch(e) {
data = { nightShift: undefined, autoToggleAt: 0 };
data = { nightShift: true, autoToggleAt: 0 };
saveThemeData(data);
}
return data;
Expand Down
357 changes: 357 additions & 0 deletions _posts/2022-04-21-React-디자인.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,357 @@
---
layout: post
title: TWL-03 React 디자인
subtitle: React의 디자인에 대해서 설명한다.
categories: [TWL, React]
tags: [TWL, React, Design]
---

## 새로운 언어나 프레임워크

언어나 프레임워크는 도구이기 때문에 목적에 따라 새로운 언어나 프레임워크를 사용하게 된다.
하지만 타 언어나 프레임워크를 쓰던 사람들은 자신의 스타일대로 새로운 언어나 프레임워크를 사용한다.
하지만 해당 언어나 프레임워크를 쓰던 사람이 그런 코드를 보면 **"이건 파이썬스럽지 않아"**, **"이건 go스럽지 않아"**
같은 말을 주로 한다.

각 언어나 프레임워크 별로 디렉터리를 구성하는 방법, 코드를 작성하는 스타일이 다르기 때문에
이런 부분은 새 언어를 배우면서 함께 터득해야한다.

굳이 새 언어나 프레임워크의 스타일대로 굳이 코드를 작성하지 않더라도 원하는 동작은 실행시킬 수 있지만
그 언어나 프레임워크 기능과 특징을 제대로 활용하지 못할 가능성이 크다. "~스럽다"는 것은 그 언어의 특징을 잘 살리도록
코드를 작성한다는 의미이기 때문이다. 보통 언어나 프레임워크스럽게 코드를 작성하면 성능이 좋거나 가독성이 좋아진다.
우리는 언어나 프레임워크를 사용할 때 그 언어나 프레임워크스럽게 코드를 작성하도록 노력해야한다.

## 언어나 프레임워크스럽게

React는 자바스크립트로 프론트엔드를 만들 수 있도록 도와주는 라이브러리다. 하지만 CRA를 통해
SPA로 리액트 코드를 작성하게 되면서 거의 프레임워크처럼 다루게 된다.
주로 언어나 프레임워크를 사용할 때 스타일이 드러나는 것은 다음과 같다.

1. 디렉터리 구조
2. 상태 관리
3. 작성 스타일

디렉터리 구조는 생각보다 언어나 프레임워크의 스타일을 잘 드러낸다.
보통 개발하다보면 관심사 별로 파일이나 디렉터리를 분리하게 되는데 디렉터리 구조는 이 언어나 프레임워크에서
어떻게 관심사를 분리해서 개발하는 지 쉽게 알 수 있다.

프로젝트를 개발하다 보면 서비스를 제공하기 위해 어떠한 상태가 저장되어야 할 필요성이 생긴다.
주로 대부분의 정보는 서버에 저장하고 화면에 보여줄 정보들만 프론트엔드에서 가지고 있는다.
이때 상태를 다루는 방법도 다양하다.

백엔드에서는 서버에서 캐싱을 위해 특정 메모리에 저장해두는 경우도 있고 전역 캐싱을 위해 레디스를 사용하기도 한다.
혹은 영구적인 저장이 필요하다면 데이터베이스에 저장한다.
만약 세션의 요청에 정보가 유지되어야 한다면 `context`에 상태를 저장해 실행되는 함수에 넘겨 모든 depth에서
접근할 수 있도록 만들기도 한다.

여기서 중요한 것은 각각의 사용 목적이 다르다는 것이다. 각 관리 방식을 다른 목적으로도 사용할 수 있지만
그렇게 되면 효율이 떨어진다. 예를 들어 캐싱을 위해 메모리가 아닌 데이터베이스에 저장하게 된다면 성능이 확연히 떨어지게 될 것이다.

작성 스타일은 그 언어만의 특색에 가까워 자주 사용하거나 관련 프로젝트를 진행하면서 습득하게 된다.
예를 들어 자바스크립트에서 `is_call && call()`은 `is_call`을 확인했을 때 값이 참일 때에만 컴파일러가
`&&`의 뒷 부분을 실행한다는 최적화를 이용해 if문을 생략한 코드이다.
이 코드는 다른 언어에서도 사용할 수 있지만 유독 자바스크립트에서 많이 보이는 스타일이다.
그리고 보통 이렇게 해결할 수 있는 코드를 다른 방법으로 해결하면 "어.. js스럽지않은데.."라는 말을
간혹 듣게 된다.
> 파이썬스러운 코드의 예제: `evens = [n for n in numbers if n % 2]`
> for range와 if문으로 위 코드와 같은 동작을 하게 만들어 파이썬스럽지 않다는 말을 들어봤다..

## 리액트스럽게

이번 포스트에서는 디렉터리 구조와 리액트에서 상태관리를 다루는 `state`, `props`, `context`, 그리고
React의 안티패턴들을 다룰 예정이다.

### 리액트의 디렉터리 구조

```
- src/
- components/
- List/
- index.js
- Header/
- index.js
- Content/
- index.js
- Comment/
- index.js
```

리액트 프로젝트를 시작하기 위해 많은 개발자들이 CRA를 실행한다. 프로젝트 생성이 완료되면 또 다시 많은 개발자들이
`src` 폴더에 `components` 폴더를 기계적으로 만든다. 사실 CRA에서는 `components` 폴더까지 같이 만들어줘야 하지 않나 싶다.

리액트에서는 각 컴포넌트들을 개발해 합성해 더 큰 컴포넌트를 만들거나 페이지를 만들어내기 때문에
`components` 폴더에 개발하는 컴포넌트를 모아서 관리하게 된다. 예를 들어 블로그 페이지를 구성한다고 하면
글 목록을 위한 `List`, 포스트를 위한 `Header`, `Content`, 댓글을 위한 `Comment`를 `components`폴더에서
관리하게 될 것이다.

```
- src/
- pages/
- Main/
- index.js
- Post/
- index.js
- store/
- user/
- index.js
- post/
- index.js
```

앞서 말했듯이 CRA는 SPA를 만들게 된다. 개발하다 보면 각 페이지에 대해 rotuer로 이동하게 만들게된다.
이런 경우를 위하여 `pages`를 만들어 페이지 별로 관리해두면 편리하다.

또한 `redux`, `mobx`등의 다양한 상태 관리 도구를 이용해 글로벌 상태를 관리하기도 하는데 이런 경우에는
주로 `store` 디렉터리에서 관리할 관심사 별로 디렉터리를 분리해 관리하기도 한다.

```
- src/
- assets/
- components/
- List/
- index.js
- Header/
- index.js
- Content/
- index.js
- Comment/
- index.js
- feature/
- Message/
- index.js
- Payment/
- index.js
- pages/
- Main/
- index.js
- Post/
- index.js
- store/
- user/
- index.js
- post/
- index.js
- utils/
```

전체적으로 보면 위와 같은 디렉터리 구조가 될 것이다.

### 리액트의 상태 관리

리액트에서 상태를 다룰 때 기본적으로 사용하는 것은 하위 컴포넌트로 값을 넘기기 위한 `props`, 상태를 저장하기 위한 `state` 가 있다.
컴포넌트를 개발하다보면 큰 단위의 컴포넌트가 있고 컴포넌트 전체의 상태 정보를 상위 컴포넌트에서 `state`로 저장해 관리하게 된다.
하지만 상태 정보는 하위 컴포넌트에서도 화면을 구성하기 위해 필요한 정보다. 그렇기 때문에 `props`를 넘겨
하위 컴포넌트로 넘겨주게 됩니다. 물론 하위 컴포넌트에서 `props`가 변경가능하다면 상태를 추적하기 어렵기 때문에
`props`는 `immutable`한 값으로 넘겨진다.

개념적으로는 굉장히 간단하지만 결국 모든 것은 프로젝트가 커지면서 문제가 발생하게 된다.
`props`는 값을 넘기기 위해 사용하지만 depth가 깊은 상위 컴포넌트의 정보가 props로 전달되어야 할 수도 있다.
넘기는 것 자체는 어려운 문제가 아니지만 중간에 있는 모든 컴포넌트들에 불필요하게 props가 전달되어야 하는 문제가 발생한다.

이때 사용할 수 있는 것이 `context API` 혹은 `redux` 와 같은 상태 관리 도구다.

#### props

리액트의 props는 프로그래밍 언어의 함수 인자와 대응된다. 일반적인 함수 인자와 다른 점은 readonly라는 점이다.
함수의 인자와 대응된다는 말은 함수의 인자를 넘길 때와 비슷한 문제가 발생한다는 말이다.

함수에서 인자가 너무 많아지면 이해하기 가독성이 떨어지듯이 props가 늘어나면 가독성이 떨어진다.

함수에 값으로 넘기는 인자가 많을 때 해결 방법은 크게 두가지다. 인자를 하나의 객체로 묶어 넘기거나, 함수의 기능을 더 분리하는 것이다.
하지만 리액트의 props는 대부분 화면에 보여질 값들이기 때문에 객체로 묶어 전달하는 것은 오히려 위험할 수도 있다.
모든 값이 전부 사용되는 값이라고 한다면 문제가 없지만 값이 누락되거나 이후 수정에서 더이상 사용하지 않는 값이 생길 수도 있기 때문에
되도록이면 객체로 넘기지 않는 것이 좋다.

```javascript
<UserView name={name} age={age} address={address} gender={gender} education={education}/>

const userProps = {age: 25, address: '서울', gender: '남', education: '대졸'};
<UserView userProps={userProps} />

// 혹시 누락된다면...
const userProps = {age: 25, address: '서울'};
<UserView userProps={userProps} />
```

위와 같은 예제처럼 객체로 넘기게 되면 실수로 필드를 빠뜨릴 때 잘못된 화면을 구성할 수 있기 때문에
리액트에서는 객체로 묶어 넘기기보다는 차라리 컴포넌트를 더 작게 분리하는 것이 좋을 수도 있다.

하지만 props를 적게 넘길 수 있는 경우도 있다. 바로 함수를 props로 넘길 때다.
함수를 props로 넘기는 경우는 하위 컴포넌트에서 어떤 상호작용을 할 때 상위 컴포넌트의 값을 변경해야하는 경우다.
주로 `setState`를 하위 컴포넌트로 넘기는 경우다.

```javascript
function SubComponent({addItem, removeItem, clearItem}) {
...
}

function Component() {
const [items, setItems] = useState([]);
const addItem = (item) => {
setItems([...items, item])
}
const removeItem = (item) => {
setItems([...items, item])
}
const clearItems = (item) => {
setItems(items.filter(x => x !== item));
}
...
return (
<>
...
<SubComponent addItem={addItem} removeItem={removeItem} clearItem={clearItems}/>
</>
);
}
```

위와 같은 경우에는 `setItems`를 사용하는 함수들을 `SubComponent`의 함수를 상위 컴포넌트에서 구현해서 넘겨주고 있다.
구현을 위해 상위 컴포넌트에서 하위 컴포넌트를 위한 함수를 구현하고 있다. 좋은 프로그래밍을 위해서는 각 컴포넌트는 자신의
컴포넌트에서 기능을 구현해야한다.

만약 위의 코드에서 `SubComponent`에 새로운 기능을 구현하려 한다면 상관 없는 `Component`에서 새롭게 다시 구현해야하는 문제가 발생한다.

```javascript
function SubComponent({items, setItems}) {
const addItem = (item) => {
setItems([...items, item])
}
const removeItem = (item) => {
setItems([...items, item])
}
const clearItems = (item) => {
setItems(items.filter(x => x !== item));
}
...
}

function Component() {
const [items, setItems] = useState([]);
...
return (
<>
...
<SubComponent items={items} setItems={setItems}/>
</>
);
}
```

이런 문제를 막기 위해 상위 컴포넌트에서는 state를 다루기 위한 `state`와 `setState`를 넘기게 되면 더이상 하위 컴포넌트의
기능에 영향을 받지 않게 된다. 이후 `SubComponent`에서 `setState` 함수를 이용해 구현하게 되면 새로운 기능이 하위 컴포넌트에
추가되더라도 해당 컴포넌트에서만 구현이 추가되는 것으로 더이상 영향이 없게된다.

#### state

state는 위의 props 예제에서도 볼 수 있듯이 각 컴포넌트에서의 상태 정보를 저장하기 위해 사용한다.
state는 컴포넌트에서 하위 컴포넌트까지 영향을 주게 된다.

```javascript
function func3(value) {
const a = value;
}

function func2(value) {
func3(value);
}

function func1() {
const value = 100;
func2(value);
}
```

상위 컴포넌트의 state는 props를 통해 하위 컴포넌트에 영향을 줄 수 있고 상위 컴포넌트의 state의 변화가 하위 컴포넌트에도
영향을 주도록 만든다.

#### context API

context라는 이름은 주로 어떠한 상태를 다음 함수 호출에서 가질 수 있도록 상태를 저장해 넘길 때 주로 사용한다.
리액트에서는 하위 컴포넌트에 직접 props로 넘기지 않더라도 값을 하위 컴포넌트에서 사용할 수 있게 만드는 것이다.

```javascript
function SubComponent2({items, setItems}) {
...
}

function SubComponent1({items, setItems}) {
...
return (
<>
...
// 단순히 props를 pass하기만 함
<SubComponent2 items={items} setItems={setItems}/>
</>
);
}

function Component() {
const [items, setItems] = useState([]);
...
return (
<>
...
<SubComponent items={items} setItems={setItems}/>
</>
);
}
```

리액트 개발을 하다보면 상위의 state가 하위에서 필요한 경우가 많아지는데 하위 컴포넌트와의 depth가 길어지는 경우가 많다.
이때 depth동안 props의 값을 사용하지 않지만 하위 컴포넌트에서 사용하기 때문에 값을 넘겨줘야 하는 경우나
props가 너무 많아지는 경우가 문제가 된다.

이렇게 props가 너무 길어지는 경우를 props drilling이라고 말하는데 이 문제를 해결하기 위해 context 를 사용하게 된다.

```javascript
function func1() {
const value = 100;
function func2() {
function func3() {
const a = value;
}
}
}
```

context의 원리는 위와 같다. `func1`에서 사용되는 state인 `value`는 하위 함수인 `func2`와 `func3`의 scope에서
접근할 수 있기 때문에 상위 컴포넌트의 `value`를 접근할 수 있다. 하지만 `func1`의 외부에서는 값을 접근하지 못하게 된다.

context를 사용할 때도 위와 같은 경우에 사용해야한다.

```javascript
const MyStore = createContext([]);

function SubComponent() {
return (
<MyStore.Consumer>
{value => <div>{value}</div>}
</MyStore.Consumer>
)
}

function Component() {
const [items, setItems] = useState([]);
return (
<MyStore.Provider value={items}>
<SubComponent/>
</MyStore.Provider>
);
}
```

사용할 때 props와 마찬가지로 state와 함께 사용할 수 있다. 이 때 props 인자로 직접 넘겨주지 않더라도 context 를 이용해
`Provider`와 `Consumer`에서 값을 지정하고 받아와 사용할 수 있다.

state와 context를 이용해 전역상태도 만들 수 있다.
하지만 전역상태를 관리하기 위한 redux, mobx 같은 도구 대신에 context API를 사용할 이유는 없다.

component에서 단순히 props를 줄이기 위한 용도라면 context를 사용해도 좋다.
하지만 상태를 관리하며 일부 상태의 변경만 적용되는 등 최적화된 작업을 원한다면 redux와 같은 상태관리 도구를 사용하는 것이 더욱 좋은 선택이 될것이다.


## Reference

* [https://www.robinwieruch.de/react-folder-structure/](https://www.robinwieruch.de/react-folder-structure/)
* [https://marvelapp.com/blog/making-good-component-design-decisions-in-react/](https://marvelapp.com/blog/making-good-component-design-decisions-in-react/)