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

[한소정] 4회차 완 #16

Open
wants to merge 3 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
56 changes: 56 additions & 0 deletions 04회/sojeong/4-1.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# 4회차 - 1. 렌더링하고 커밋하기

# 렌더링이란?

> **“Rendering” is React calling your components.**
>

흔히 “렌더링”을 실제로 react가 화면에 컴포넌트를 그리는 것이라고 생각 하기 쉽다.
하지만 렌더링은 컴포넌트가 브라우저에 표시 되는 과정이 아니라 컴포넌트를 **호출**하여 화면에 표시할 내용을 파악하는 과정이다. 즉 **렌더링**은 **리액트**가 **컴포넌트**를 **호출** 하는 것이다.

*참고: 리액트에서 컴포넌트의 렌더링은 재귀적으로 일어난다. 컴포넌트가 중첩되어 있다면 더 이상 nested 된게 없을 때까지 렌더링이 계속된다. 이 때문에 부모 컴포넌트의 렌더링이 자식 컴포넌트의 렌더링까지 촉발 하게 되는 것.

# UI가 렌더링 되는 과정



> 순서
>
> 1. **Triggering** a render (delivering the guest’s order to the kitchen)
> 2. **Rendering** the component (preparing the order in the kitchen)
> 3. **Committing** to the DOM (placing the order on the table)

렌더링 조건에 따라 렌더링이 trigging 되고 → 리액트는 컴포넌트를 호출해서 표시할 내용 및 변경 사항(재렌더링일 시)을 파악 후 → DOM을 실제로 조작한다.

<aside>
💡 React의 Virtual DOM은 실제로 그냥 DOM을 직접 조작 하는 것보다 빠를까?

- 클릭

답은 “아니오.” 좋은 글이 있어 첨부하니 참고하면 되겠다.

[[번역] 리액트에 대해서 그 누구도 제대로 설명하기 어려운 것 - 왜 Virtual DOM 인가?](https://velopert.com/3236)

[React 가 Virtual DOM 을 사용하는 이유](https://velog.io/@woohm402/virtual-dom-and-react)

</aside>

# 언제 리액트는 컴포넌트를 렌더링 하나?

> 두가지 이유
>
> 1. It’s the component’s **initial render.**
> 2. The component’s (or one of its ancestors’) **state has been updated.**

리액트는 두가지 경우에 컴포넌트를 렌더링한다.
하나는 첫 렌더링인 경우.(which is obvious :p) 그리고 조상(상위 요소)중 하나의 state가 변경된 경우.
root.render() 메소드로 DOM 루트에 진입 후 첫 렌더링이 일어나게 되고 이후에는 state가 변경 되는 경우 재렌더링이 일어난다.

# DOM 수정 (변경사항 커밋)

> 초기 렌더링인 경우와 리렌더링인 경우
>
> - **For the initial render,** React will use the `[appendChild()](https://developer.mozilla.org/docs/Web/API/Node/appendChild)` DOM API to put all the DOM nodes it has created on screen.
> - **For re-renders,** React will apply the minimal necessary operations (calculated while rendering!) to make the DOM match the latest rendering output.

초기 렌더링에서는 DOM 트리가 새로 만들어지게 되고, 리렌더링의 경우에 리액트는 이전과 비교 했을 때 달라진 부분만 DOM을 변경한다. 여기서 Virtual DOM의 장점이 나오게 되는 것이다. 수많은 DOM 조작이 일어나지만 모든 연산이 끝난 후의 결과로 실제의 DOM 계산을 딱 한번 수행 하고 이 과정을 라이브러리를 사용하는 사람은 알 필요 없게 해주는 것. React를 쓰는 이유라고 할 수 있다.
27 changes: 27 additions & 0 deletions 04회/sojeong/4-2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# 4회차 - 2. 스냅샷으로서의 state ~ 내용 전체

# state는 특별한 기능이 있는 변수일까?

그래서 react가 이를 감지하고 재렌더링을 하는걸까? setState로 state를 변경하면 그 변경은 언제 일어나게 될까? react의 useState는 어떻게 각자의 state가 대응하는 setter를 사용할지를 알까? 이번 장에서는 이런 의문들에 대해 답을 해보도록 하자.

# setState를 하면 일어나는 일

> You might think of your user interface as changing directly in response to the user event like a click. In React, it works a little differently from this mental model. On the previous page, you saw that [setting state requests a re-render](https://react-ko.dev/learn/render-and-commit#step-1-trigger-a-render) from React. This means that for an interface to react to the event, you need to *update the state*.
>

useState의 반환값으로 받아온 setState를 사용하여 state를 변경하면 그 즉시 값이 변경 되는 것으로 오해할 수 있다. 하지만 react의 멘탈 모델은 그것과는 조금 다르다. setState가 호출 되었을때 리액트는 리렌더링을 “요청” 한다. 그러면 그 값을 큐에 담아두었다가, 다음 렌더링 때 새로운 값에 따라 컴포넌트를 리렌더링 하게 되는 것이다.

# **Rendering takes a snapshot in time**

> [“Rendering”](https://react-ko.dev/learn/render-and-commit#step-2-react-renders-your-components) means that React is calling your component, which is a function. The JSX you return from that function is like a snapshot of the UI in time. Its props, event handlers, and local variables were all calculated **using its state at the time of the render.**
>

이게 무슨 말일까? 스냅샷을 찍는다?
사실 렌더링이 일어날 때 컴포넌트 내부의 state는 절대 변하지 않는다. state는 렌더링이 일어나는 시점의 그 state 값으로 “고정”되어있다. 이건 사실 꽤나 중요한 멘탈모델이자 패러다임이다. 이렇게 동작 하기 때문에 우리는 React Hook이라는 것을 쓸 수 있는 것이고, 함수형 컴포넌트와 클래스형 컴포넌트의 차이도 생긴다.
이에 대한 insight를 얻었던 글과 유용하게 읽었던 글을 첨부하겠다. 나머지 모든 챕터의 내용이 링크의 글들을 참고하면 이해 될 것이다!

[useEffect 완벽 가이드](https://overreacted.io/ko/a-complete-guide-to-useeffect/)

[함수형 컴포넌트와 클래스, 어떤 차이가 존재할까?](https://overreacted.io/ko/how-are-function-components-different-from-classes/)

[React hooks: not magic, just arrays](https://medium.com/@ryardley/react-hooks-not-magic-just-arrays-cd4f1857236e)
260 changes: 260 additions & 0 deletions 10회/한소정/10.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,260 @@
# Effect 의존성 제거하기

## useEffect 의존성 배열에 빈 배열을 써도 될까?

혹시 생각 해본적 있는가? 린터 오류를 무시하기 위해 이유를 모른채 빈배열을 일단 써오지는 않았는가? 오늘은 이 주제에 대해서 얘기해보려 한다.

## 의존성 배열이란?

> When you write an Effect, the linter will verify that you’ve included every reactive value (like props and state) that the Effect reads in the list of your Effect’s dependencies. This ensures that your Effect remains synchronized with the latest props and state of your component. Unnecessary dependencies may cause your Effect to run too often, or even create an infinite loop. Follow this guide to review and remove unnecessary dependencies from your Effects.

Effect가 실행될 때 react는 useEffect의 두번째 인자로 들어가는 배열에 들어가있는 값이 바뀌었는지를 확인하고 바뀌었다면 재실행한다. 이 배열을 의존성 배열이라고 하는데 linter가 Effect가 사용하는 모든 props, state와 같은 반응형 값을 포함하고 있는지를 확인한다고 설명이 써있다.
그래서 빈 배열을 주면

```
Lint Error
11:6 - React Hook useEffect has a missing dependency: 'roomId'. Either include it or remove the dependency array.
```

라는 오류가 발생하게된다.

그럼 왜 빈배열을 주면 오류를 발생시키게 만들어놨을까?

## 의존성에게 거짓말을 하지 마라

```
useEffect(() => {
document.title = 'Hello, ' + name;
}, [name]);
```

위의 코드에서 의존성 배열은 이펙트 내에서 쓰이는 모든 값을 가지고있다. 이로써 이펙트는 언제 다시 이펙트를 실행해야 할지 알고있다. 가령 name이 dan에서 yuji로 바뀌게 되면 의존성이 다르기때문에 이펙트를 재실행한다.
하지만 저 배열에 []를 줬다면, 이펙트는 두번 다시 새로 실행 되지 않는다.

이런 경우는 어떨까? 매 초마다 숫자가 올라가는 카운터를 작성하고싶다. 그러면 이런 생각을 할 수 있다. "이펙트를 한 번만 설정하고, 한 번만 제거하자." 그럼 이런 코드가 나온다.

```
function Counter() {
const [count, setCount] = useState(0);

useEffect(() => {
const id = setInterval(() => {
setCount(count + 1);
}, 1000);
return () => clearInterval(id);
}, []);
return <h1>{count}</h1>;
}
```

"이게 처음 한 번만 실행 되었으면 좋겠어." 하고 빈배열을 넣었다.
이 코드의 실행 결과를 예상해보라. 어떨 것 같은가?
이 예제는 카운트를 단 한번만 증가시킨다.
만약 우리의 멘탈모델이 "의존성 배열은 내가 언제 이펙트를 다시 실행해야할지 지정해야할 때 쓰인다." 라면, 위 예제를 볼 때 자가당착에 빠지게 된다. 이건 인터벌이니까 한 번만 실행하고 싶다. 왜 이런 문제가 일어나게 됐을까?

의존성배열은 리액트에게 이 이펙트에서는 이 값만을 쓰겠다고 약속하는 것이다. 그러므로 이펙트에서 쓰이는 값은 모두 알려줘야한다. 하지만 위 예제는 어떤가? count를 쓰면서 쓰지 않는다고 거짓말을 했다. 여기서 이게 어떤식으로 문제가 될까?

첫번째 렌더링에서 count는 0이다. 따라서 첫번째 렌더링의 이펙트에서 setCount(count + 1)은 setCount(0 + 1)이라는 뜻이 된다. 의존성배열을 []라고 정의했기 때문에 이펙트를 절대 다시 실행하지 않고, 결국 그로 인해 매 초마다 setCount(0 + 1)을 호출하는 것이다.

```
// 첫 번째 렌더링, state는 0
function Counter() {
// ...
useEffect(
// 첫 번째 렌더링의 이펙트
() => {
const id = setInterval(() => {
setCount(0 + 1); // 언제나 setCount(1)
}, 1000);
return () => clearInterval(id);
},
[] // 절대 다시 실행하지 않는다
);
// ...
}

// 매번 다음 렌더링마다 state는 1이다
function Counter() {
// ...
useEffect(
// 이 이펙트는 언제나 무시될 것
// 왜냐면 리액트에게 빈 deps를 넘겨주는 거짓말을 했기 때문
() => {
const id = setInterval(() => {
setCount(1 + 1);
}, 1000);
return () => clearInterval(id);
},
[]
);
// ...
}
```

따라서 의존성배열을 []로 지정하는 것은 버그를 만들 것이다. 그리고 이런 종류의 이슈는 해결책을 떠올리기 어렵다. 그러므로 의존성 배열에게 솔직하게 값을 알려주는 것을 중요한 규칙으로 받아들여야한다.

그러면 어떻게 솔직하게 쓸까?

## 의존성을 솔직하게 적는 방법

두가지 전략이 있다. 하나는 컴포넌트 안에 있으면서 Effect안에서 쓰이는 모든 값을 배열에 넣는 것이고 하나는 이펙트의 코드를 바꿔서 우리가 원하던 것 보다 자주 바뀌는 값을 요구하지 않도록 만드는 것이다.

일반적으로 첫번째 방법을 사용해보고, 필요하다면 두번째 방법을 이용하는데 주제가 의존성 제거하기인만큼 후자를 설명해보겠다.

## 의존성을 더 적게 넘겨주기

몇가지 기술을 살펴보자.

### 이펙트가 자급자족하도록 만들기

```
useEffect(() => {
const id = setInterval(() => {
setCount(count + 1);
}, 1000);
return () => clearInterval(id);
}, [count]);
```

여기서 count를 어떻게 제거할까?<br>
그 전에 한번 질문을 해보자. 왜 count를 쓰고있는가?<br>
오로지 setCount를 위해 쓰고있는 것으로 보인다. 이 경우 스코프안에서 count를 쓸 필요가 전혀 없다. 이전상태를 기준으로 상태값을 업데이트 하고싶을때는 setState에 함수형태의 업데이터를 사용하면 된다.

```
useEffect(() => {
const id = setInterval(() => {
setCount(c => c + 1);
}, 1000);
return () => clearInterval(id);
}, []);
```

setCount(count + 1)이라고 썼기때문에 count는 분명 이펙트 안에서 필요한 의존성이었다. 하지만 우리는 단지 count를 count + 1로 변환하여 리액트에게 "돌려주기"를 원했을 뿐이다.<br>
하지만 리액트는 현재의 count를 이미 알고있다. 우리가 리액트에게 알려줘야 하는것은 지금 값이 뭐든 간에 상태값을 + 1 하라는 것이다.
<br>
그것이 setCount(c => c + 1)이 의도하는 것이다. 리액트에게 상태가 어떻게 바뀌어야하는지 "지침을 보내는 것" 이라고 생각할 수 있다.<br>
꼼수를 쓴게 아니다. 실제로 이펙트는 더이상 렌더링 스코프에서 count값을 읽어들이지 않는다.
<br>
이 이펙트가 한번만 실행 되었다 하더라도 첫번째 렌더링에 포함되는 인터벌 콜백은 인터벌이 실행될 때마다 c => c + 1이라는 지침을 완벽하게 전달한다. 더이상 현재의 count 상태를 알고 있을 필요가 없다. 리액트가 이미 알고있으니까.
<br>

### 함수를 이펙트 안으로 옮기기

```
function SearchResults() {
const [data, setData] = useState({ hits: [] });

async function fetchData() {
const result = await axios(
'https://hn.algolia.com/api/v1/search?query=react',
);
setData(result.data);
}

useEffect(() => {
fetchData();
}, []); // 이거 괜찮은가?
// ...
```

일단 이 코드는 동작한다. 하지만 간단히 로컬 함수를 의존성에서 제외하는 해결책은 컴포넌트가 커지면서 모든 경우를 다루고 있는지 보장하기 아주 힘들다는 문제가 있다.
<br>
각 함수가 5배정도는 커져서 코드를 이런 방식으로 나누었다고 생각해보자.

```
function SearchResults() {
// 이 함수가 길다고 상상해보자
function getFetchUrl() {
return 'https://hn.algolia.com/api/v1/search?query=react';
}

// 이 함수도 길다고 상상해보자
async function fetchData() {
const result = await axios(getFetchUrl());
setData(result.data);
}

useEffect(() => {
fetchData();
}, []);

// ...
}
```

이제 나중에 이 함수들 중에 하나가 state나 prop을 사용한다고 생각해보자.

```
function SearchResults() {
const [query, setQuery] = useState('react');

// 이 함수가 길다고 상상해보자
function getFetchUrl() {
return 'https://hn.algolia.com/api/v1/search?query=' + query;
}

// 이 함수가 길다고 상상해 보자
async function fetchData() {
const result = await axios(getFetchUrl());
setData(result.data);
}

useEffect(() => {
fetchData();
}, []);

// ...
}
```

만약 이런 함수를 사용하는 단 하나의 이펙트에서라도 의존성 배열을 업데이트 하는 것을 깜빡했다면 이펙트는 props와 state 동기화에 실패할 것이다.<br>
다행히도 이 문제를 해결할 쉬운 방법이 있다. 어떠한 함수를 이펙트 안에서만 쓴다면, 그 함수를 "직접 이펙트 안으로" 옮겨라.

```
function SearchResults() {
// ...
useEffect(() => {
// 아까의 함수들을 안으로 옮겼다!
function getFetchUrl() {
return 'https://hn.algolia.com/api/v1/search?query=react';
}
async function fetchData() {
const result = await axios(getFetchUrl());
setData(result.data);
}
fetchData();
}, []); // ✅ 의존성은 OK
// ...
}
```

이러면 뭐가 좋냐? 우리는 더이상 "옮겨지는 의존성"에 신경쓸 필요가 없다. 의존성 배열은 더이상 거짓말 하지 않는다.<br>
**진짜로 이펙트 안에서 컴포넌트의 범위 바깥에 있는 그 어떠한 것도 사용하고 있지 않다.**
<br>
나중에 getFetchUrl 을 수정하고 query state를 써야한다고 하면, 이펙트 안에 있는 함수만 고치면 된다는 것을 쉬이 발견할 수 있다. 거기에 더해 query 를 이펙트의 의존성으로 추가해야 할 것이다.

```
function SearchResults() {
const [query, setQuery] = useState('react');

useEffect(() => {
function getFetchUrl() {
return 'https://hn.algolia.com/api/v1/search?query=' + query;
}

async function fetchData() {
const result = await axios(getFetchUrl());
setData(result.data);
}

fetchData();
}, [query]); // ✅ 의존성은 OK
// ...
}
```

이 의존성을 더하는 것이 단순히 “리액트를 달래는” 것은 아니다. query 가 바뀔 때 데이터를 다시 페칭하는 것이 말이 된다. useEffect 의 디자인은 사용자가 제품을 사용하다 겪을 때까지 무시하는 대신, 데이터 흐름의 변화를 알아차리고 이펙트가 어떻게 동기화해야할지 선택하도록 강제한다. 그게 린터가 하는 일인것이다.

### 함수를 이펙트 안에 넣고싶지 않다면?

시간관계상 생략