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

[ 3주차 생각 과제 ] #8

Merged
merged 1 commit into from
Nov 17, 2023
Merged
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
333 changes: 333 additions & 0 deletions week3.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,333 @@
## ▶️ 컴포넌트 분리

🐵 개인적으로 프론트엔드 개발자에게 가장 중요한 능력 중 하나가 **컴포넌트 분리, 모듈화** 라고 생각한다!

컴포넌트는 리액트 웹을 구성하는 **단위**인 만큼 **한가지의 역할**만 해야한다

그러나 그렇다고 해서 최대한 쪼개서 컴포넌트를 생성하는 것은 성능상 바람직하지 않다.

모듈화의 핵심은 **재사용성**이기 때문이다.

🙋‍♀️ **그렇다면 왜 재사용할 내용을 컴포넌트화 해주어야 할까?**

가장 단적인 예시로 하나의 웹앱 내에 동일한 디자인의 버튼이 이곳 저곳에서 텍스트만 달리하여 사용될 것이다.

그런데, 해당 서비스가 브랜딩을 다시하게 되면서 디자인 시스템도 달라지게 될 경우, 웹 앱 내의 모든 버튼의 디자인도 변경될 것이다.

이때 버튼을 컴포넌트화하지 않고 매번 각기 다른 element로 생성해두었다면, 그 모든 버튼을 찾아다니며 각각 디자인을 변경해주어야 할 것이다.

그러나, 만약 하나의 버튼 컴포넌트를 만들고 이를 이곳 저곳에서 재사용하고 있었다면,

그 하나의 컴포넌트의 스타일만 변경해주면 동시에 웹앱 내의 모든 버튼의 스타일을 깔끔히 업데이트시켜줄 수 있다.

정리하자면, 컴포넌트는 **한가지의 일**을 **반복적**으로 하는 친구를 **재사용**하기 위해 묶어둔 모듈이다 !

이러한 방식을 Computer Science 에서는 **‘관심사의 분리’, Separation of Concerns (SoC)** 디자인 원칙이라고 한다.

위의 내용에서 이야기한 것에 따라,

결국 컴포넌트 분리의 기준은 다음과 같다.

✔️ **한가지의 역할을 하는가?**

✔️ **반복 사용되고 있거나, 반복 사용될 가능성이 있는가?**

✔️ **UI**와 관련된 일을 하는가? **기능(로직)**과 관련된 일을 하는가?

위의 기준에 따라 나의 컴포넌트가 더 분리될 수 있는 길이 있는지 끊임없이 고민하며

모듈화하고자 노력해보자 ! !

---

## ▶️ 상태관리 & Props Drilling

### 🙋‍♀️ **React에서 상태란?**

- 컴포넌트 간에 이동되는 `props`와 달리 컴포넌트 내부에서 관리되는 자바스크립트 객체
- 어플리케이션의 화면 렌더링에 영향을 끼친다
- 사용자의 인터랙션을 통해 **동적으로 변하는** 데이터

### 🐵 **상태의 종류**

- 지역 상태 : 컴포넌트 내부에서만 관리되는 상태
- ex) Input을 통해 사용자 입력을 받아 상태를 업데이트하는 경우
- 컴포넌트 간 상태 : 여러 컴포넌트를 넘나들며 관리되는 상태
- 부모 컴포넌트에서 자식 컴포넌트로 prop를 통해 전달한다 → 연속될 경우 “Prop Drilling”
- ex) Modal창
- 전역 상태 : 프로젝트 전체에 영향을 끼치는 상태

### 🐵 **클라이언트 상태 vs 서버 상태**

| | Client State | Server State |
| ------------- | --------------------------------------------------------------------- | -------------------------------------------------------------------------- |
| 위치 | 클라이언트에 저장된 데이터 | 서버나 외부 데이터 소스에 저장된 데이터 |
| 접근성 | 클라이언트만 접근 가능 | 접근 권한이 있는 모든 클라이언트 접근 가능 |
| 데이터 관리 | 클라이언트에서 관리 <br/> (Redux, Recoil 등 상태관리 라이브러리 사용) | 서버에서 관리 <br/>(데이터베이스 사용) |
| 지속성 | 세션 간에 필연적으로 지속되진 않음 | 일반적으로 세션 간에 지속성 유지 |
| 네트워크 요청 | 데이터를 가져오거나 업데이트하려면 네트워크 요청이 필요할 수 있음 | 데이터 접근 또는 업데이트를 위해 네트워크 요청이 필요할 수 있음 |
| 보안 | 클라이언트가 액세스할 수 있고 보안 수준이 낮음 | 인증 및 암호화로 보호할 수 있으므로 더 안전 |
| 성능 | server state에 비해 속도 빠름 | client state에 비해 속도 느림 |
| 확장성 | 클라이언트 기기 용량이 제한적이어서 확장성이 다소 떨어짐 | 전용 서버나 데이터 소스로 관리되기 때문에 용량이 커서 확장성이 비교적 높음 |
| 예시 | 컴포넌트 state, Redux state, 브라우저 쿠키 | DB 레코드, API 응답, 서버 세션 데이터 |

### 🐵 **Props Drilliing**

- props를 다른 컴포넌트로 전달하기 위해서 **여러 컴포넌트를 거치는 방식**을 말한다.
> 부모 컴포넌트 → 중간 컴포넌트1 → 중간 컴포넌트 2→ … → 최종 컴포넌트
> 부모 컴포넌트에서 최종 컴포넌트로 props를 전달하기 위해 중간 컴포넌트들을 거친다.
- 이 depth가 깊어질 수록 상태를 **추적하기 어려워**지기 때문에 무조건 props drilling의 방법으로 props를 전달하기보다 props들이 불필요한 컴포넌트들을 거치지 않도록 설계하거나, 관리하는 것이 반드시 필요하다.

### 🐵 **상태를 관리해주자**

**서로 다른 컴포넌트**에서 **동일한 데이터**를 다룰 때! 해당 상태를 관리해주어야 한다.

🙋‍♀️ **왜?**

서로 다른 컴포넌트에서 동일한 데이터를 다룰 땐, 해당 데이터의 **출처가 동일**해야 한다. 그래야 서로의 **변화에 동적으로 반응**할 수 있기 때문이다.

즉 **상태의 일관성 (=”데이터의 무결성”)**을 잘 지켜야 한다.

**“Single Source of Truth”**이라는 방법론이 이러한 상태의 일관성을 유지하기 위해 등장한 방법론인데, 말그대로 신뢰할 수 있는 **단일 출처**를 말한다.

이 방법론이 React가 택한 방법론으로, 결국 React에서 상태관리가 필요한 이유는 데이터의 무결성을 위해서, 서로 다른 컴포넌트에 분포되어있는 하나의 데이터가 서로 일치할 수 있도록 관리해주기 위함이다.

### 🐵 **상태 관리 라이브러리**

Props Driling이 일어나지 않도록, 상태가 효과적으로 사용될 수 있도록 효율적인 설계를 하는 것이 가장 중요하지만,

만약 최선을 다해 설계해도 어쩔 수 없이 불필요한 Props Drilling이 많이 발생할 경우 **상태 관리 라이브러리**를 잘 활용하는 것도 좋다.

상태관리 라이브러리는 Client State / Server State에 따라 분류할 수 있다.

대표적인 몇가지의 라이브러리에 대해 알아보자.

💡 **Client State Library - Context API**

- 구성요소
- Context : 전역 상태 저장소.
- Context에 Provider와 Consumer가 저장되어있음
- Consumer에는 Context를 통해 접근 가능
- Provider : 전역 상태 제공자.
- 저장소인 Context에 상태를 제공해주는 역할을 함
- 상태를 제공받고자 하는 컴포넌트는 Provider 하위에 있어야함
- 따라서 전역 상태로 쓸 경우, Provider는 App.jsx같은 루트 컴포넌트가 된다.
- Consumer : 전역 상태 사용자
- **단점 :**
- 범위를 잘 정의해주지 않으면 불필요한 렌더링을 일으킨다

💡 **Client State Library - Redux**

- 중앙 집중식 Storage와 상태 업데이트를 위한 Reducer를 사용함
- 단방향 데이터 흐름을 따른다.
- **장점 :**
- 오래된 만큼 탄탄한 커뮤니티와 개발자 풀 존재
- 개발을 처음 시작할 때 참고할 수 있는 보일러플레이트가 굉장히 다양할 뿐만 아니라, 개발 중에 있어서도 버그가 생길 경우 해결하기가 쉬움
- 미들웨어를 활용하여 여러 비동기, 로그 작업 등을 처리할 수 있는데 이 때 Redux를 위한 라이브러리를 사용하여 더 쉽게 구현 가능
- 단방향 데이터 흐름을 따라서 Reducer 등의 단위 테스트가 비교적 쉬움
- **단점 :** 대부분 구조가 너무 복잡해서 문제가 생김
- 간단한 웹앱을 만들 때에도 Action, Reducer, Action Creator 등의 코드를 모두 작성해야 함.
- Recoil, MobX와는 달리 State가 변경 될 때 Component를 업데이트 해주는 반응형 메커니즘이 기본적으로 탑재되지 않아서 React의 자체 메커티즘을 활용하거나 추가적인 외부 라이브러리를 사용해야 함

💡 **Client State Library - Recoil**

- 비교적 최근에 나온 새로운 상태 관리 라이브러리
- Facebook사에서 개발
- 가장 React스러운 상태관리라는 철학을 갖고있음
- **장점 :**
- Redux, MobX에 비해 더 간단한 구조를 가져서 초심자가 시작하기에 적당하며, 작은 프로젝트를 시작하는 데 과도한 보일러 플레이트가 필요하지 않음
- component가 렌더링 되는 시기, 상태 등을 세밀하게 제어 가능 (성능 최적화 등에도 활용 가능)
- Redux와는 달리 Reactive 메커니즘을 탑재하고 있기에, 동적인 기능을 조금 더 쉽게 구현 가능
- **단점 :**
- 최신 라이브러리이다 보니, 사용자 커뮤니티가 비교적 빈약함 → 이슈가 생겼을 때, 홀로 해결해야 하는 경우가 생길 수도.
- Recoil의 상태관리 자체가 굉장히 세분화 되어 있기에 초심자가 시작하기엔 쉽더라도, 디버깅 하거나 테스트를 진행하기에 어려울 수도 있습니다.

💡 **Client State Library - MobX**

- Redux의 여러 점을 보완하여 나온 상태관리 라이브러리
- Redux보다 조금 더 객체 지향적이며, Immutable.js 와 같은 불변성을 유지하기 위한 라이브러리를 굳이 사용할 필요가 없다는 특징
- **장점 :**
- Redux에 비해 전체적으로 조금 더 쉬운 러닝커브를 가짐
- 객체 지향적이고 캡슐화를 지원하기에 개발자 친화적인 편
- Redux에서 제공하지 않는 반응형 메커니즘을 제공하기에 조금 더 쉽게 동적 웹앱 제작 가능
- **단점 :**
- 웹앱 규모가 커지면서 로직이 MobX의 자동 업데이트에 의존하기에 디버깅이 조금 더 어려워질 수 있음
- Redux 만큼 커뮤니티가 크지 않음
- Validation 구현에 있어 코드가 조금 번잡스럽다고 알려져 있음

💡 **Server State Library - React Query**

React에서 `데이터 fetching`과 `캐싱 프로세스`를 간소화해주는 **라이브러리**

- 외부(API 등)로부터 데이터를 fetching 및 업데이트 과정 간소화
- API 요청의 로딩 및 오류 상태 관리
- 캐싱 자동 관리

Client에서 API와 통신을 하기 위해서는
추가적으로 **useEffect**, **useState**를 함께 쓰면서 데이터를 fetching 해왔다.

하지만 React Query를 사용하면 이러한 훅들의 필요성이 사라진다.

React Query에 대해 자세히 알아보자 !

✔️ **React Query 주요 개념**

`Query`

- API 엔드포인트, DB 등의 원격 데이터 소스로부터 데이터 요청
- **useQuery** 훅 사용

`Mutation`

- 서버에 데이터를 추가하거나 업데이트
- **useMutation** 훅 사용

`Query Caching`

- Query 결과를 메모리에 저장
`Query Invalidation`
- 쿼리를 오래된 상태로 여겨 무효화

✔️ **useQuery 훅**

> 데이터 Fetching 용

API 엔드포인트나 DB에서 데이터를 **비동기적**으로 가져오도록 서버에 요청하는 것

useQuery는 기본적으로

- **컴포넌트 최초 mount 시**에 데이터를 fetching해오고,
- 이후의 데이터 업데이트에 대해서는 **자동 실행되지 않는다. (데이터 refetching 안해줌)**

✔️ **useMutation 훅**

API 엔드포인트나 DB에 데이터를 새로 생성, 수정, 삭제

✔️ **Query Caching**

원격 서버와 통신하는데에 걸리는 시간을 단축하고자,

여러번 불러올 데이터는 캐시에 저장하여 반복해서 필요로 할 경우 원격 서버가 아닌 캐시에서 빠르게 가져올 수 있는 방식

useQuery 훅 사용 시, 지정했던 **`고유 키`** 밑에 **반환된 데이터가 캐싱**된다.

기본적으로는 캐시 데이터는 **오래된(stale) 상태**로 기록된다.

**✔️ Query Invalidation**

지정해준 `staleTime` 과 무관하게 즉시 데이터를 stale 상태로 처리해야하는 경우가 있다.

예를 들어, API에 POST 요청을 보내 데이터 값을 업데이트할 때엔, API 엔드포인트에 있는 데이터의 값이 캐시에 저장되어있는 값보다 더 최신 상태가 되고, 캐시에 저장되어있는 데이터는 곧바로 outdated한 값이 되어버린다. 이럴 때엔 즉시 캐시 데이터를 stale 상태로 처리해줘야 한다.

> React Query의 QueryClient 객체의 invalidateQueries 메소드 활용

- **모든 쿼리**, 혹은 고유 키를 통해 **특정 쿼리**를 stale 상태로 처리해주는 역할

---

### ▶️ 렌더링

렌더링을 효과적으로 관리함은 결국 **성능 최적화**에 직결되기 때문에 반드시 필요하다.

다음과 같은 방법들을 고려하여 우리의 프로젝트를 지끈지끈하지 않도록 만들어주자!

**✔️ useMemo**

- useMemo는 종속 변수들이 변하지 않으면 함수를 굳이 다시 호출하지 않고 이전에 반환한 참조값을 재사용 한다.
- 즉, 함수 호출 시간도 세이브할 수 있고 같은 값을 props로 받는 하위 컴포넌트의 리렌더링도 방지할 수 있다.

✔️ **React.memo 컴포넌트 메모이제이션**

- React hook이 아니라서 클래스형 컴포넌트에서도 사용할 수 있다.
- React.memo를 통해 컴포넌트의 props가 바뀌지 않았다면, 리렌더링하지 않도록 설정하여 함수형 컴포넌트의 리렌더링 성능을 최적화 해줄 수 있다.
- 콜백함수를 이용해 메모이제이션을 적용할지 여부를 판단할 수도 있다

✔️ **useCallback**

- useMemo가 리턴되는 값을 memoize 시켜주었는데, useMemo와 비슷한 useCallback은 **함수 선언을 memoize 하는데 사용된다**
- useCallback으로 함수를 선언해주면, 종속 변수들이 변하지 않는 이상 굳이 함수를 재생성하지 않고 이전에 있던 참조 변수를 그대로 하위 컴포넌트에 props로 전달하여, 하위 컴포넌트도 props가 변경되지 않았다고 인지하게 되어 하위 컴포넌트의 리렌더링을 방지할 수 있다.

✔️ **자식 컴포넌트의 props로 객체를 넘겨줄 경우 변형하지말고 넘겨주기**

- props의 값으로 객체를 넘겨주는 경우 새로 생성된 객체가 props로 들어가므로 컴포넌트가 리렌더링 될 때마다 새로운 객체가 생성되어 자식 컴포넌트로 전달된다.
- props로 전달한 객체가 동일한 값이어도 새로 생성된 객체는 이전 객체와 다른 참조 주소를 가진 객체이기 때문에 자식 컴포넌트는 메모이제이션이 되지않는다.
- 생성자 함수나 객체 리터럴로 객체를 생성해서 하위 컴포넌트로 넘겨주는 방식이 아닌, state를 그대로 하위컴포넌트에 넘겨주어 필요한 데이터 가공을 그 하위컴포넌트에서 해주는 것이 좋다.

✔️ **컴포넌트를 매핑할 때에는 key값으로 index를 사용하지 않는다.**

- 리액트에서 매핑을 할때 반드시 고유 key를 부여하도록 강제하고 있는데, 배열의 index값으로 key값을 부여하면 좋지 않다.
- 왜냐하면, 어떤 배열에 중간에 어떤 요소가 삽입될때 그 중간 이후에 위치한 요소들은 전부 index가 변경된다.
- 이로 인해 key값이 변경되어 React는 key가 동일 할 경우, 동일한 DOM Element를 보여주기 때문에 예상치 못한 문제가 발생한다. 또한, 데이터가 key와 매치가 안되어 서로 꼬이는 부작용도 발생한다.

✔️ **useState의 함수형 업데이트**

- setState를 사용할 때 새로운 상태를 파라미터로 넣는 대신, 상태 업데이트를 어떻게 할지 정의해 주는 업데이트 함수를 넣을 수도 있는데,
이렇게 하면 useCallback을 사용할 때 두 번째 파라미터로 넣는 배열에 값을 넣어주지 않아도 된다.

```jsx
// 예시) 삭제 함수
const onRemove = useCallback(
(id) => {
setTodos(todos.filter((todo) => todo.id !== id));
},
[todos]
);

// 예시) 함수형 업데이트 후
const onRemove = useCallback((id) => {
setTodos((todos) => todos.filter((todo) => todo.id !== id));
}, []);
```

✔️ **Input에 onChange 최적화**

- 보통 input 태그에 onChange 이벤트를 줄때 타이핑을 할때마다 해당 컴포넌트가 렌더링 되어, 최적화 방법을 많이 찾곤한다.
- lodash 라고 최적화 라이브러리를 쓰기도 하는데, 아래 코드는 라이브러리를 쓰지 않고 최적화 시킬수 있는 방법이다.

```jsx
// 예시) 최적화 전(X)
//UserList.jsx
function UserList() {
{...}
return (
<div>
<input
type="text"
value={text}
placeholder="아무 내용이나 입력하세요."
onChange={(event) => setText(event.target.value)}
/>
{...}
</div>
);
}

export default UserList;

// 예시) 최적화 후(O)
//UserList.jsx
function UserList() {
{...}
return (
<div>
<input
ref={searchRef}
type="text"
placeholder="아무 내용이나 입력하세요."
onKeyUp={() => {
let searchQuery = searchRef.current.value.toLowerCase();
setTimeout(() => {
if (searchQuery === searchRef.current.value.toLowerCase()) {
setText(searchQuery);
}
}, 400);
}}
/>
{...}
</div>
);
}

export default UserList;

```