diff --git a/README.md b/README.md index 63696fa..c2a5809 100644 --- a/README.md +++ b/README.md @@ -1,2 +1 @@ -- 스터디 Docs - 링크 +- 스터디 Docs diff --git a/pages/2week/doyeon/study.mdx b/pages/2week/doyeon/study.mdx index bba3fd8..760eee2 100644 --- a/pages/2week/doyeon/study.mdx +++ b/pages/2week/doyeon/study.mdx @@ -132,29 +132,21 @@ DOM은 웹페이지에 대한 인터페이스로 브라우저가 웹페이지의 📍 **생명주기 메서드** -| render() | 컴포넌트가 UI를 렌더링하기 위해 쓰임 -부수 효과가 없어야 한다. (state를 변경하는 일 등) | -| --- | --- | +| render() | 컴포넌트가 UI를 렌더링하기 위해 쓰임. 부수 효과가 없어야 한다. (state를 변경하는 일 등) | +| :--- | :--- | | componentDidMount() | 컴포넌트가 마운트되고 준비되는 즉시 실행 | -| componentDidUpdate() | 컴포넌트 업데이트가 일어난 이후 바로 실행 -state나 props의 변화에 따라 DOM을 업데이트하는 등에 쓰인다. | -| componentWillUnmount() | 컴포넌트가 언마운트되거나 더 이상 사용되지 않기 직전에 호출 -이벤트, 타이머를 지우거나, API 호출 취소 등의 작업을 하는 데 유용하다. | -| shouldComponentUpdate() | 컴포넌트에 영향을 받지 않는 변화에 대해 정의 -state나 props의 변경으로 리액트 컴포넌트가 다시 리렌더링되는 것을 막을 때 사용한다. | -| static getDerivedStateFromProps() | render()를 호출하기 직전에 호출 -static으로 선언돼 있어 this에 접근할 수 없다. | -| getSnapShotBeforeUpdate() | DOM이 업데이트되기 직전에 호출 -윈도우 크기를 조절하거나 스크롤 위치를 조정하는 등의 작업을 하는 데 유용하다. | +| componentDidUpdate() | 컴포넌트 업데이트가 일어난 이후 바로 실행. state나 props의 변화에 따라 DOM을 업데이트하는 등에 쓰인다. | +| componentWillUnmount() | 컴포넌트가 언마운트되거나 더 이상 사용되지 않기 직전에 호출. 이벤트, 타이머를 지우거나, API 호출 취소 등의 작업을 하는 데 유용하다. | +| shouldComponentUpdate() | 컴포넌트에 영향을 받지 않는 변화에 대해 정의. state나 props의 변경으로 리액트 컴포넌트가 다시 리렌더링되는 것을 막을 때 사용한다. | +| static getDerivedStateFromProps() | render()를 호출하기 직전에 호출. static으로 선언돼 있어 this에 접근할 수 없다. | +| getSnapShotBeforeUpdate() | DOM이 업데이트되기 직전에 호출. 윈도우 크기를 조절하거나 스크롤 위치를 조정하는 등의 작업을 하는 데 유용하다. | 📍 **에러 상황에서 실행되는 메서드** -| static getDerivedStateFromError() | 자식 컴포넌트에서 에러가 발생했을 때 호출되는 에러 메서드 -반드시 state 값을 반환해야 한다. | -| --- | --- | +| static getDerivedStateFromError() | 자식 컴포넌트에서 에러가 발생했을 때 호출되는 에러 메서드. 반드시 state 값을 반환해야 한다. | +| :--- | :--- | | componentDidCatch | getDerivedStateFromError에서 에러를 잡고 state를 결정한 이후에 실행된다. | -![Untitled](https://prod-files-secure.s3.us-west-2.amazonaws.com/ef0e9d83-accc-43c2-9de2-d86a3439e204/1c445d2a-6038-4935-a0f2-614228f3329a/Untitled.png) 📍 **클래스형 컴포넌트의 한계** @@ -221,15 +213,15 @@ static으로 선언돼 있어 this에 접근할 수 없다. | ## 2.5 컴포넌트와 함수의 무거운 연산을 기억해 두는 메모이제이션 -> ***메모이제이션** +> **메모이제이션** 연산의 결과값을 메모리에 저장해두고 이전 값과 결과가 동일할 때 재사용하는 기법 -**React에서 제공하는 메모이제이션 기법*** +**React에서 제공하는 메모이제이션 기법** > > -> `*React.memo()`는 props의 값으로 변경을 확인한다.* +> `React.memo()`는 props의 값으로 변경을 확인한다. > -> `*useCallback()`과 `useMemo()`는 dependency 배열 내부의 값으로 변경사항을 확인한다.* +> `useCallback()`과 `useMemo()`는 dependency 배열 내부의 값으로 변경사항을 확인한다. > ### 2.5.1 주장 1: 섣부른 최적화는 독이다. 꼭 필요한 곳에만 메모이제이션을 추가하자 diff --git a/pages/4week/0uizi0/flux.png b/pages/4week/0uizi0/flux.png new file mode 100644 index 0000000..8aa107e Binary files /dev/null and b/pages/4week/0uizi0/flux.png differ diff --git a/pages/4week/0uizi0/study.mdx b/pages/4week/0uizi0/study.mdx index 1c8f9aa..8acbe0b 100644 --- a/pages/4week/0uizi0/study.mdx +++ b/pages/4week/0uizi0/study.mdx @@ -1 +1,129 @@ -## 정리하기 파일 +## 4장. 서버 사이드 렌더링 + +### SPA vs SSR + +- SPA + - 렌더링과 라우팅에 필요한 대부분의 기능을 서버가 아닌 브라우저의 자바스크립트에 의존하는 방식 + - 최초에 서버에서 최소한의 데이터를 불러온 이후로부터는 이미 가지고 있는 자바스크립트 리소스와 브라우저 API를 기반으로 모든 작동이 이뤄짐 + - JAM(JavaScript, API, Makup) 스택 등장 ⇒ 서버 확장성 문제에서 자유로워짐 + - 장점 : 훌륭한 사용자 경험 제공 ← 한 번 로딩된 이후에는 서버를 거쳐 필요한 리소스를 받아올 일이 적어지기 때문 + - 단점 : 최초에 로딩해야 할 자바스크립트 리소스가 커짐 +- SSR + - 최초에 사용자에게 보여줄 페이지를 서버에서 렌더링해 빠르게 사용자에게 화면을 제공하는 방식 + - 장점 + - 최초 페이지 진입이 비교적 빠름 + - 검색 엔진과 SNS 공유 등 메타데이터 제공이 쉬움 + - 누적 레이아웃 이동이 적음 : 사용자가 예상치 못한 시점에서 페이지가 변경되어 불편을 초래하는 것 + - 사용자의 디바이스 성능에 비교적 자유로움 + - 좀 더 안전한 보안 + - 단점 + - 소스코드를 작성할 때 항상 서버를 고려해야 함 + - 적절한 서버가 구축돼 있어야 함 + - 서비스 지연에 따른 문제 + +### SSR을 위한 리액트 API + +- **renderToString** + - 서버 사이드 렌더링을 구현하는 데 가장 기초적인 API + - 인수로 넘겨받은 리액트 컴포넌트를 렌더링해 HTML 문자열로 반환하는 함수 + - 뛰어난 초기 렌더링 성능 ← 먼저 완성된 HTML을 서버에서 제공할 수 있으므로 + - 검색 엔진과 SNS 공유 등 메타데이터 제공이 쉬움 + - data-reactroot : hydrate 함수에서 루트를 식별하는 기준점이 됨 +- **renderToStaticMarkup** + - renderToString 함수와 유사하지만, 리액트에서만 사용하는 추가적인 DOM 속성을 만들지 않는다는 차이가 있음 + - 리액트의 이벤트 리스너가 필요 없는 완전히 순수한 HTML을 만들 때만 사용됨 +- **renderToNodeStream** + - renderToString과 결과물이 완전히 동일 + - renderToString과 달리 브라우저에서 사용 불가능 + - 결과물의 타입이 ReadableStream으로 Node.js에서만 사용 가능 + - 스트림 : 큰 데이터를 다룰 때 데이터를 청크(chunk, 작은 단위)로 분할해 조금씩 가져오는 방식 +- **renderToStaticNodeStream** + - renderToNodeStream과 제공하는 결과물은 동일하나, 리액트 자바스크립트에 필요한 속성이 제공되지 않음 +- **hydrate** + - renderToString과 renderToNodeStream으로 생성된 HTML 콘텐츠에 자바스크립트 핸들러나 이벤트를 붙이는 역할 + - reder와 인수를 넘기는 방식이 유사함 + - 이미 렌더링된 HTML을 기준으로 이벤트를 붙이는 작업만 실행 ↔ render : 빈 HTML에 정보를 렌더링 + - 서버에서 완성한 HTML과 하이드레이션 대상이 되는 HTML의 결과물이 동일한지 비교하며 렌더링 진행 + +--- + +### Next.js + +- **Next.js의 \_app.tsx과 \_document.tsx의 차이** + +| **\_app.tsx** | **\_document.tsx** | +| :--------------------------------------------------------------: | :--------------------------------------------------------------------: | +| Next.js를 초기화하는 파일로, Next.js 설정과 관련된 코드를 모아둠 | Next.js로 만드는 웹사이트의 뼈대가 되는 HTML 설정과 관련된 코드를 추가 | +| 경우에 따라 서버와 클라이언트 모두에서 렌더링 가능 | 반드시 서버에서만 렌더링됨 | +| ⇒ 이벤트 핸들러를 추가하는 것은 불가능 | + +- **next의 SSR과 CSR** + - 서버 라우팅 : 최초 페이지 렌더링이 서버에서 수행됨 + - 클라이언트 라우팅 : 내부 페이지 이동 방식 + - a 태그 대신 next/link를 통한 페이지 이동 + - window.location.push 대신 router.push 사용 +- **Data Fetching** + + - pages/ 폴더에 있는 라우팅이 되는 파일만 사용할 수 있음 + - 예약어로 지정되어 반드시 정해진 함수명으로 export를 사용해 함수를 파일 외부로 내보냄 + - 서버에서 미리 필요한 페이지를 만들어 제공 가능 + - 해당 페이지에서 요청이 있을 때마다 서버에서 데이터를 조회해 미리 페이지를 만들어 제공할 수 있음 + +--- + +## 5장. 리액트와 상태 관리 라이브러리 + +### 상태 관리는 왜 필요한가? + +**Flux 패턴의 등장** + +![flux-pattern](./flux.png) + +- 양방향 데이터 바인딩으로 인한 상태 변경 코드 추적이 어려움을 문제의 원인으로 판단, 단방향 데이터 바인딩 방식 도입 +- `action` : 어떠한 작업을 처리할 액션과 그 액션 발생 시에 함께 포함시킬 데이터 +- `dispatcher` : 액션을 스토어에 보내는 역할 +- `store` : 액션의 타입에 따라 값을 어떻게 변경할지가 정의됨 +- `view` : 스토어에서 만들어진 데이터를 가져와 화면을 렌더링 + +**Redux** + +- Flux 구조를 구현한 라이브러리로, 데이터의 흐름을 단방향으로 강제함 +- ELM 아키텍처 도입 : `model`, `view`, `update` +- 하나의 상태 객체를 스토어에 저장해두고, 이 객체를 업데이트하는 작업을 디스패치해 업데이트를 수행함 +- props-drilling 문제 해결 +- 하고자 하는 일에 비해 보일러플레이트가 너무 많다는 단점이 존재 + +**Context API** + +- props-drilling 문제를 해결하고 싶지만, Redux의 보일러플레이트가 부담스러워 등장하게 된 기능 +- 상태 주입을 도와주는 기능(상태관리X)으로, 렌더링을 막아주는 기능이 존재하지 않음 + +**React Query, SWR** + +- HTTP 요청에 특화된 상태 관리 라이브러리 + +**Recoil, Zustand, Jotai, Vatio** + +- 훅을 활용해 작은 크기의 상태를 효율적으로 관리 + +### 리액트 훅으로 시작하는 상태관리 + +- **useState와 useReducer** : `지역 상태`로, 해당 컴포넌트 내에서만 유효하다는 한계 존재 + +- **Recoil** + + - atom(최소한의 상태 단위)은 key 값을 필수로 가짐 + + - selector 함수를 사용해, 한 개 이상의 atom 값을 바탕으로 새로운 값을 조립할 수 있음 +- **Jotai** + - 작은 단위의 상태를 위로 전파하는 구조 + - Recoil의 한계를 보완한 라이브러리 + - Recoil의 atom 개념을 도입하면서도 API가 간결 + - Recoil과 달리 selector 함수 없이도 atom만으로 또 다른 파생된 atom 생성 가능 + - 타입이 잘 지원되며, 리액트 18의 변경된 API를 원활하게 지원 + - atom 생성 시, 별도의 key를 넘겨주지 않음 (객체를 키로 활용하는 WeakMap 방식 활용) +- **Justand** + - 하나의 스토어를 중앙 집중형으로 활용해 스토어 내부에서 상태를 관리 (리덕스와 비슷) + - partial : state의 일부분만 변경하고 싶을 때 사용 + - replace : state를 완전히 새로운 값으로 변경하고 싶을 때 사용 + - API가 복잡하지 않고 사용이 간단헤 쉽게 접근할 수 있음 diff --git a/pages/4week/doyeon/study.mdx b/pages/4week/doyeon/study.mdx index 1c8f9aa..30f8083 100644 --- a/pages/4week/doyeon/study.mdx +++ b/pages/4week/doyeon/study.mdx @@ -1 +1,210 @@ ## 정리하기 파일 +# 04장. 서버 사이드 렌더링 + +## 4.1 서버 사이드 렌더링이란? + +### 4.1.1 싱글 페이지 애플리케이션의 세상 + +**싱글 페이지 애플리케이션(Single Page Application; SPA)이란?** + +SPA이란 렌더링과 라우팅에 필요한 대부분의 기능을 서버가 아닌 브라우저의 자바스크립트에 의존하는 방식을 의미한다. +페이지를 불러온 이후에는 서버에서 HTML을 내려받지 않고 하나의 페이지에서 모든 작업을 처리하므로 싱글 페이지 애플리케이션이라고 한다. + +**싱글 페이지 애플리케이션의 단점** + +최초에 로딩해야 할 자바스크립트 리소스가 커진다. + +**싱글 페이지 애플리케이션의 장점** + +한번 로딩된 이후에는 서버를 거쳐 필요한 리소스를 받아올 일이 적어지기 때문에 사용자에게 훌륭한 UI/UX를 제공한다. + +**싱글 페이지 렌더링 방식의 유행과 JAM 스택의 등장** + +과거 PHP나 JSP(JavaServer Pages)를 기반으로 대부분의 웹 애플리케이션이 만들어졌을 때는 거의 대부분의 렌더링이 서버 사이드에서 이뤄졌다. +프레임워크의 등장으로 등장한 것이 JAM(JavaScript, API, Markup)스택이다. 대부분의 작업을 자바스크립트에서 수행할 수 있었기 때문에 프런트엔드는 자바스크립트와 마크업(HTML, CSS)을 미리 빌드해 두고 정적으로 사용자에게 제공하면 이후 작동은 모두 사용자의 클라이언트에서 실행되므로 서버 확장성 문제에서 좀 더 자유로워질 수 있게 됐다. + +### 4.1.2 서버 사이드 렌더링이란? + +서버 사이드 렌더링은 최초에 사용자에게 보여줄 페이지를 렌더링해 빠르게 사용자에게 화면을 제공하는 방식을 의미한다. + +**서버 사이드 렌더링의 장점** + +1. 최초 페이지 진입이 비교적 빠르다. +2. 검색 엔진과 SNS 공유 등 메타데이터 제공이 쉽다. +3. 누적 레이아웃 이동이 적다. +4. 사용자의 디바이스 성능에 비교적 자유롭다. +5. 보안에 좀 더 안전하다. + +**서버 사이드 렌더링의 단점** + +1. 소스코드를 작성할 때 항상 서버를 고려해야 한다. + + 브라우저 전역 객체인 window 또는 sessionStorage와 같이 브라우저에만 있는 전역 객체의 접근을 최소화해야 하고, window 사용이 불가피하다면 해당 코드가 서버 사이드에서 실행되지 않도록 처리해야 한다. 외부에서 의존하고 있는 라이브러리도 마찬가지다. + +2. 적절한 서버가 구축돼 있어야 한다. +3. 서비스 지연에 따른 문제 + +### 4.1.3 SPA와 SSR을 모두 알아야 하는 이유 + +서버 사이드 렌더링 역시 만능이 아니다. + +웹페이지에서 사용자에게 제공하고 싶은 내용이 무엇인지, 어떤 우선순위에 따라 페이지의 내용을 보여줄지를 잘 설계하는 것이 중요하다. + +현대의 서버 사이드 렌더링 + +요즘의 서버 사이드 렌더링은 최초 웹사이트 진입 시에는 서버 사이드 렌더링 방식으로 서버에서 완성된 HTML을 제공받고, 이후 라우팅에서는 서버에서 내려받은 자바스크립트를 바탕으로 마치 싱글 페이지 애플리케이션처럼 작동한다. + +## 4.2 서버 사이드 렌더링을 위한 리액트 API 살펴보기 + +리액트는 브라우저 자바스크립트 환경에서 렌더링할 수 있는 방법을 제공하지만 동시에 리액트 애플리케이션을 서버에서 렌더링할 수 있는 API도 제공한다. + +리액트레서 서버 사이드 렌더링을 실행할 때 사용되는 API를 확인해 보려면 리액트 저장소의 [react-dom/server.js](https://ko.legacy.reactjs.org/docs/react-dom-server.html)를 확인하면 된다. + +### 4.2.1 renderToString + +```jsx +const result = ReactDOMServer.renderToString( + React.createElement('div', { id: 'root' }, ) +) + +// result는 다음과 같은 문자열을 반환 +
+
hello
+ +
+``` + +- `renderToString`은 인수로 넘겨받은 리액트 컴포넌트를 렌더링해 HTML 문자열로 반환하는 함수다. +- `renderToString`을 사용하면 클라이언트에서 실행되지 않고 먼저 완성된 HTML을 서버에서 제공할 수 있으므로 초기 렌더링에서 뛰어난 성능을 보일것이다. +- 리액트의 루트 엘리먼트에 data-reactroot 속성이 있다. + +### 4.2.2 renderToStaticMarkup + +```jsx +const result = ReactDOMServer.renderToStaticMarkup( + React.createElement('div', { id: 'root' }, ) +) + +// result는 다음과 같은 문자열을 반환 +
+
hello
+
    +
  • apple
  • +
  • banana
  • +
  • peach
  • +
+
+``` + +- 인수로 넘겨받은 리액트 컴포넌트를 렌더링해 HTML 문자열로 반환하는 함수다. +- 루트 요소에 추가한 data-reactroot와 같은 리액트에서만 사용하는 추가적인 DOM 속성을 만들지 않는다.(HTML의 크기를 줄일 수 있음) +- `renderToStaticMarkup`은 이벤트 리스너가 필요 없는 완전히 순수한 HTML을 만들 때만 사용된다. + +### 4.2.3 renderToNodeStream + +- `renderToSrging`과 `renderToStaticMarkup`은 브라우저에서도 실행할 수는 있지만 `renderToNodeStream`은 브라우저에서 사용하는 것이 완전히 불가능하다. +- `renderToNodeStream`은 완전히 Node.js 환경에 의존하고 있다. +- `renderToNodeStream`의 결과물은 Node.js의 `ReadaleStream`이다. `ReadaleStream`은 utf-8로 인코딩된 바이트 스트림으로, Node.js 환경에서만 사용할 수 있다. +- 스트리을 활용한다면 브라우저에 제공해야 할 큰 HTML을 작은 단위로 쪼개 연속적으로 작성함으로써 리액트 애플리케이션을 렌더링하는 Node.js 서버의 부담을 덜 수 있다. +- 대부분의 리액트 서버 사이드 렌더링 프레임워크는 모두 `renderToNodeStream` 을 채택하고 있다. + +### 4.2.4 renderToStaticNodeStream + +- `renderToNodeStream`의 결과물은 동일하나, `renderToStaticMarkup` 과 마찬가지로 리액트 자바스크립트에 필요한 리액트 속성이 제공되지 않는다. + +### 4.2.5 hydrate + +- 생성한 HTML 콘텐츠에 자바스크립트 핸들러나 이벤트를 붙이는 역할 + +## 4.3 Next.js 톺아보기 + +**a 태그로 이동** + +- 네트워크에 hello라는 이름의 문서 요청 +- 이후에 webpack, framework, main, hello 등 페이지를 만드는 데 필요한 모든 리소스를 처음부터 다 가져옴 +- 서버에서 렌더링을 수행하고 클라이언트에서 hydrate하는 과정에서 한 번 더 실행 + +**next/Link로 이동** + +- hello.js만 존재, 나머지 요청은 없음 +- 서버 사이드 렌더링이 아닌, 클라이언트에서 필요한 JS만 불러온 뒤 라우팅하는 클라이언트 라우팅/렌더링 방식으로 작동 + +Next.js는 서버 사이드 렌더링의 장점과 싱글 페이지 애플리케이션의 장점을 모두 살리기 위해 이런 방식으로 작동 + + + +# 05장. 리액트와 상태 관리 라이브러리 + +## 5.1 상태 관리는 왜 필요할까? + +### Flux pattern + +웹앱이 복잡해지고 상태도 많아지며 **어디서 어떤 일이 일어나서** 상태가 변했는지 등을 추적하기 어려워졌다. + +이는 Flux 패턴에서 **단방향 데이터 흐름**을 따르기 때문이다. + +- action: 상태에 변화가 필요할 때 발생하는 이벤트 혹은 객체 +- dispatcher: action을 발생시키는 함수 +- store: 애플리케이션의 상태를 저장하는 곳 +- view: 사용자에게 비춰지는 화면 + +Flux 패턴에서는 action이 발생하면 dispatcher가 action을 받아서 store에 전달한다. 그리고 store는 action에 따라 상태를 변경하고 변경된 상태를 view에 전달한다. view는 변경된 상태를 화면에 렌더링한다. + +이렇듯 데이터나 상태, 액션 등을 **단방향으로 전달**하는 방식을 사용하여 Flux 패턴은 복잡한 데이터 흐름을 단순하게 만들어준다. + +### Redux + +Redux는 Flux 패턴을 기반으로 만들어진 라이브러리이다. + +**장점** + +- 상태를 더 직관적으로 관리할 수 있다. +- 복잡한 상태 관리를 쉽게 할 수 있다. + +**단점** + +- Redux를 사용하면서 불편한 점이 있다면, 액션 타입, 액션 생성 함수, 리듀서를 일일이 만들어야 한다는 점이다. + +### Context API + +- Context API는 리액트 v16.3에 새로 도입된 기능이다. +- Context API를 사용하면 Context를 만들어서 Context 안에 Provider와 Consumer를 설정할 수 있다. + +### react-query, swr + +- react-query와 swr은 서버 상태를 관리하는 라이브러리이다. + +## 5.2 리액트 훅으로 시작하는 상태관리 + +오랜 시간동안 리액트 애플리케이션의 상태 관리를 위해 리덕스에 의존했다. 그러나 현재는 새로운 Context API, useReducer, useState의 등장으로 컴포넌트에 결처셔 재사용하거나 컴포넌트 내부에 걸쳐서 상태를 관리할 수 있는 방법들이 점차 많이 등장하기 시작했고, 리덕스 외의 다른 라이브러를 선택하는 경우도 많아지고 있다. + +### Recoil + +Recoil과 Jotai는 Context와 Provider, 훅을 기반으로 가능한 작은 상태를 효율적으로 관리하는 것에 초점을 맞추고 있다. 그리고 Zustand는 리덕스와 비슷하게 하나의 큰 스토어를 기반으로 상태를 관리하는 라이브러리이다. Recoil,Jotai와는 다르게 스토어의 상태가 변경되면 해당 상태를 구독하는 컴포넌트에 전파해 리렌더링을 알린다. + +RecoilRoot는 크게 3가지의 단계로 나뉜다. + +- RecoilRoot의 AppContext에는 Recoil의 상태값들이 담긴다. +- 스토어의 상태값에 접근 할 수 있는 함수들로 상태의 읽기나 쓰기를 할 수 있다. +- 값의 변경이 있을 때 구독중인 모든 하위 컴포넌트에 값의 변화를 알린다. + +atom은 상태를 나타내는 Recoil의 최소 단위이다. + +### Jotai + +recoil과 비슷한 api 형태지만 recoil의 한계를 극복하기 위해 만들어졌다. + +- key를 사용하지 않는다. +- atom 그 자체로 파생값을 만들 수 있다. + +**Zustand** + +Redux와 비슷한 api 형태를 가지고 있다. + +- 중앙 집중형 저장소 활용 +- 바로 custom hook 느낌으로 사용 가능 +- 보일러 플레이트 코드가 Flux, Redux 보다 적다. \ No newline at end of file diff --git a/pages/4week/jkyeun/about.mdx b/pages/4week/jkyeun/about.mdx index df3be89..b0e9c48 100644 --- a/pages/4week/jkyeun/about.mdx +++ b/pages/4week/jkyeun/about.mdx @@ -1 +1,3 @@ -## 이야기해보기 파일 +- 이번 내용은 별 내용이 없는거 같아요 . . . +- 제가 저번에 발표한 내용과도 겹치는게 좀 있는거 같아요. +- 상태관리 라이브러리 각각의 사용법은 사실 공식문서가 최고죠! diff --git a/pages/4week/jkyeun/study.mdx b/pages/4week/jkyeun/study.mdx index 1c8f9aa..c03643b 100644 --- a/pages/4week/jkyeun/study.mdx +++ b/pages/4week/jkyeun/study.mdx @@ -1 +1,108 @@ -## 정리하기 파일 +# [4장] 서버 사이드 렌더링 + +## 서버 사이드 렌더링이란? + +### 싱글 페이지 애플리케이션의 세상 + +- 서버사이드 렌더링 애플리케이션과 반대되는 개념인 싱글 페이지 애플리케이션 +- `` 내부에 아무런 내용이 없다. +- 네이버 스포츠 화면은 네이버 홈과 다른 환경에서 HTML을 만들어서 제공하므로 어쩔 수 없이 처음부터 HTML을 다시 완성해야 하므로 페이지가 전환될 때 부자연스러운 모습을 보게 된다. +- 싱글 페이지 애플리케이션은 페이지 전환에 필요한 일부 영역만 다시 그리게 되므로 훨씬 더 매끄러운 UI를 보여줄 수 있게 된다. +- 과거에는 LAMP 스택, 즉 Linux(운영체제), Apache(서버), MySQL(데이터베이스), PHP/Python 등(웹 프레임워크)으로 구성돼 있었다. + - 이 LAMP 스택은 과거 매우 인기 있는 웹 개발 구조이기도 했지만 동시에 어쩔 수 없는 선택이기도 했다. + - 자바스크립트가 할 수 있는 일이 제한적이었기 떄문에 대부분의 처리를 서버에서 해야만 했기 때문이다. +- 최근 등장한 스택이 바로 JAM(JavaScript, API, Markup) 스택이다. + - JAM 스택의 인기와 Node.js의 고도화에 힘입어 MERN(MongoDB, Express.js, React, Node.js) 스택처럼 아예 API 서버 자체도 자바스크립트로 구현하는 구조도 인기를 끌기 시작했다. + +### 서버 사이드 렌더링이란? + +- 장점 + - 최초 페이지 진입이 비교적 빠르다. + - 검색 엔진과 SNS 공유 등 메타데이터 제공이 쉽다. + - 요청이 완전히 완료된 이후에 완성된 페이지를 제공하므로 완성된 HTML을 보여준다. + - 서버에서 부담을 나눌 수 있으므로 사용자의 디바이스 성능으로부터 조금 더 자유로워질 수 있다. + - 보안에 좀 더 안전하다 +- 단점 + - 브라우저 전역 객체인 window 또는 sessionStorage 등에 접근할 수 없다. + - 사용자의 요청에 따라 적절하게 대응할 수 있는 물리적인 가용량을 확보해야 하고, 때로는 예기치 않은 장애 상황에 대응할 수 있도록 복구 전략도 필요하다. (적절한 서버 구축 필요) +- 서버에서 사용자에게 보여줄 페이지에 대한 렌더링 작업이 끝나기까지는 사용자에게 그 어떤 정보도 제공할 수 없다. + +### SPA와 SSR을 모두 알아야 하는 이유 + +- 서버 사이드 렌더링은 성능에 있어 만병통치약이 아니다. +- 가장 뛰어난 싱글 페이지 애플리케이션은 가장 뛰어난 멀티 페이지 애플리케이션보다 낫다. +- 평균적인 싱글 페이지 애플리케이션은 평균적인 멀티 페이지 애플리케이션보다 느리다. +- 두 가지 모두 장단점이 있으며 어느 하나가 완벽하다고 볼 수 없다. +- 현대의 서버 사이드 렌더링 + - Next.js 등은 최초 웹사이트 진입 시에는 서버 사이드 렌더링 방식으로 서버에서 완성된 HTML을 제공받고, 이후 라우팅에서는 서버에서 내려받은 자바스크립트를 바탕으로 마치 싱글 페이지 애플리케이션처럼 작동한다. +- 프론트엔드 개발자는 서버에서의 렌더링, 그리고 클라이언트의 렌더링을 모두 이해해야 두 가지 장점을 완벽하게 취하는 제대로 된 웹서비스를 구축할 수 있다. + +## 서버 사이드 렌더링을 위한 리액트 API 살펴보기 + +### renderToString + +- renderToString을 사용하면 앞서 언급했던 서버 사이드의 이점, 클라이언트에서 실행되지 않고 일단 먼저 완성된 HTML을 서버에서 제공할 수 있으므로 초기 렌더링에서 뛰어난 성능을 보일 것이다. +- 검색 엔진이나 SNS 공유를 위한 메타 정보도 renderToString에서 미리 준비한 채로 제공할 수 있으므로 SPA 구조보다 손쉽게 완성할 수 있다. + +### renderToStaticMarkup + +- renderToString과의 유의미한 차이점은 앞서 루트 요소에 추가한 data-reactroot와 같은 리액트에서만 사용하는 추가적인 DOM 속성을 만들지 않는다는 점이다. +- renderToStaticMarkup의 결과물을 기반으로 리액트의 자바스크립트 이벤트 리스너를 등록하는 hydrate를 수행하면 서버와 클라이언트의 내용이 맞지 않다는 에러가 발생한다. 그 이유는 보다시피 renderToStaticMarkup의 결과물은 hydrate를 수행하지 않는다는 가정하에 순수한 HTML만 반환하기 때문이다. + +### renderToNodeStream + +- renderToNodeStream은 완전히 Node.js 환경에 의존하고 있다. +- stream은 큰 데이터를 다룰 때 데이터를 청크로 분할해 조금씩 가져오는 방식을 의미한다. + - 이는 유튜브 영상을 볼 때 사용자가 볼 수 있는 몇 초라도 먼저 다운로드되면 그 부분을 먼저 보여주고, 이후에 계속해서 영상을 다운로드하는 것을 가능하게 해준다. + - 스트림을 활용하면 이러한 큰 크기의 데이터를 청크 단위로 분리해 순차적으로 처리할 수 있다는 장점이 있다. + - 때문에 대부분의 널리 알려진 리액트 서버 사이드 렌더링 프레임워크는 모두 renderToString 대신 renderToNodeStream을 채택하고 있다. + +### renderToStaticNodeStream + +- renderToNodeStream과 제공하는 결과물은 동일하나, renderToStaticMarkup과 마찬가지로 리액트 자바스크립트에 필요한 리액트 속성이 제공되지 않는다. + +### hydrate + +- renderToString과 renderToNodeStream으로 생성된 HTML 콘텐츠에 자바스크립트 핸들러나 이벤트를 붙이는 역할 +- hydrate는 이처럼 정적으로 생성된 HTML에 이벤트와 핸들러를 붙여 완전한 웹페이지 결과물을 만든다. +- render와의 차이점은 hydrate는 기본적으로 이미 렌더링된 HTML이 있다는 가정하에 작업이 수행되고, 이 렌더링된 HTML을 기준으로 이벤트를 붙이는 작업만 실행한다는 것이다. + - rootElement 내부에는 ``을 렌더링한 정보가 이미 포함돼 있어야만 hydrate를 실행할 수 있는 것이다. + - 따라서 hydrate로 넘겨준 두 번째 인수에는 이미 renderToString 등으로 렌더링된 정적인 HTML 정보가 반드시 담겨 있어야 한다. + - 아무것도 없는 빈 HTML에 이 정보를 렌더링하는 render와의 차이점이 바로 이것이다. + +### etc. + +- 페이지가 온전히 서버에서 만들어졌는지 확인하려면 개발자 도구의 소스 보기를 통해 확인하면 된다. + +## Next.js 톺아보기 + +- npm 프로젝트를 볼 때는 pakage.json을 먼저 봐야 한다. +- next/link로 이동하는 경우 서버 사이드 렌더링이 아닌, 클라이언트에서 필요한 자바스크립트만 불러온 뒤 라우팅하는 클라이언트 라우팅/렌더링 방식으로 작동한다. +- 사용자가 빠르게 볼 수 있는 최초 페이지를 제공한다는 점과 싱글 페이지 애플리케이션의 장점인 자연스러운 라우팅이라는 두 가지 장점을 모두 살리기 위해 이러한 방식으로 작동한다. + +### next.config.js + +- swcMinify: SWC라는 오픈소스는 바벨의 대안 / JS 기반이 아닌 Rust로 작성, 병렬로 작업을 처리 +- basePath: URL prefix +- redirects: 특정 주소를 다른 주소로 보내고 싶을 때 사용 +- assetPrefix: 만약 next에서 빌드된 결과물을 동일한 호스트가 아닌 다른 CDN 등에 업로드 하고자 한다면 이 옵션에 해당 CDN 주소를 명시하면 된다. + +# [5장] 리액트와 상태 관리 라이브러리 + +## Flux 패턴의 등장 + +- 액션이라는 단방향으로 데이터의 흐름이 줄어드므로 데이터의 흐름을 추적하기 쉽고 코드를 이해하기가 한결 수월해진다. +- 리덕스의 등장 - 하고자 하는 일에 비해 보일러플레이트가 너무 많다는 비판 + +## 훅의 탄생, 그리고 React Query와 SWR + +- 기존에 우리가 알고 있는 상태 관리 라이브러리보다는 제한적인 목적으로, 일반적인 형태와는 다르다는 점만 제외하면 분명히 SWR이나 React Query도 상태 관리 라이브러리의 일종이라 볼 수 있다. +- useState와 useReducer 모두 약간의 구현상의 차이만 있을 뿐, 두 훅 모두 지역 상태 관리를 위해 만들어졌다. +- useSubscription을 사용하면 리액트 외부에서 관리되는 값에 대한 변경을 추적하고, 이를 리렌더링까지 할 수 있다. + +## 상태관리 라이브러리의 작동 방식 + +- useState, useReducer가 가지고 있는 한계, 컴포넌트 내부에서만 사용할 수 있는 지역 상태라는 점을 극복하기 위해 외부 어딘가에 상태를 둔다. 이는 컴포넌트의 최상단 내지는 상태가 필요한 부모가 될 수도 있고, 혹은 격리된 자바스크립트 스코프 어딘가일 수도 있다. +- 이 외부의 상태 변경을 각자의 방식으로 감지해 컴포넌트의 렌더링을 일으킨다. +- 이후 여러 상태관리 라이브러리 소개 +- npm에서 제공하는 모든 라이브러리와 마찬가지로 메인테이너가 많고 다운로드가 활발하며 이슈가 관리가 잘되고 있는 라이브러리를 선택하는 것이 좋다. diff --git a/pages/4week/kimjong22/study.mdx b/pages/4week/kimjong22/study.mdx index 1c8f9aa..18244a0 100644 --- a/pages/4week/kimjong22/study.mdx +++ b/pages/4week/kimjong22/study.mdx @@ -1 +1,123 @@ -## 정리하기 파일 +## 04. 서버 사이드 렌더링 + +### SPA vs SSR + +- 싱글 페이지 애플리케이션 + + - 최초의 첫 페이지에서 데이터를 모두 불러옴 + - 이후의 페이지 전환을 위한 모든 작업이 자바스크립트와 브라우저 history.pushState와 history.replaceState로 이루어짐 + - 사이트 렌더링에 필요한 `` 내부의 내용을 모두 자바스크립트 코드로 삽입한 이후에 렌더링 + - 이러한 작동 방식은 최초에 로딩해야 할 js 리소스가 커지는 단점이 있지만 한번 로딩된 이후에는 서버를 거쳐 리소스를 받아올 일이 적어지기 때문에 사용자에게 훌륭한 UIUX를 제공 + +- 서버 사이드 렌더링 방식 + - 렌더링에 필요한 작업을 서버에서 수행 + - 사용자 기기의 성능 영향을 받는 클라이언트 렌더링에 비해서, 렌더링을 서버에서 수행하기 때문에 비교적 안정적 + - FCP(First Contentful Paint)가 더 빨라지는 것이 장점 + - 검색 엔진과 SNS 공유 등 메타데이터 제공 용이 + - CLS 누적 레이아웃 이동을 줄일 수 있음 + - 사용자의 디바이스 성능에 비교적 자유로움 + - 보안에 좀 더 안전 + - 서버에서 렌더링 작업이 끝나기까지는 정보 제공이 불가능, 때문에 더 안 좋은 사용자 경험을 제공할 수 있다는 위험 존재 + +--- + +### SSR을 위한 리액트 API + +리액트는 리액트 애플리케이션을 서버에서 렌더링할 수 있는 API를 제공한다. 이 API는 Node.js와 같은 서버 환경에서만 실행할 수 있다. + +--- + +##### renderToString + +```javascript +function App(){ + return ( + <> +
Hello World
+ + ) +} + +const result = ReactDOMServer.renderToString( + React.createElement('div', {id: 'root'}, ), +) + +// result의 반환값 +
// data-reactroot는 hydrate 함수에서 루트를 식별하는 기준 +
Hello World
+
+``` + +- renderToString은 인수로 주어진 리액트 컴포넌트를 빠르게 브라우저가 렌더링할 수 있는 HTML을 제공하는 데 목적이 있다. +- 완성된 HTML을 서버에서 서빙하기 때문에 초기 렌더링에서 뛰어난 성능을 보임 +- 검색 엔진이나 SNS 공유를 위한 메타 정보도 renderToString에서 미리 준비한 채로 제공 + +##### renderToStaticMarkup + +- renderToString과 유의미한 차이점은 루트 요소에 추가한 data-reactroot와 같은 추가적인 DOM 속성을 만들지 않는다는 점 +- 때문에 HTML 크기를 아주 조금이라도 줄일 수 있다. + +##### renderToNodeStream + +- Node.js의 ReadableStream을 반환 + - ReadableStream은 utf-8로 인코딩된 바이트 스트림, Node.js 환경에서만 사용 가능 + - 스트림은 큰 데이터를 다룰 때 데이터를 청크로 분할해 조금씩 가져오는 방식을 의미 + - 대부분의 리액트 서버 사이드 렌더링 프레임워크는 renderToString이 아닌 renderToNodeStream를 채택 + +##### renderToStaticNodeStream + +- hydrate를 할 필요 없는 순수 HTML 결과물이 필요할 때 사용하는 메서드 + +##### hydrate + +- hydrate 함수는 renderToString이나 renderToNodeStream으로 생성된 HTML 콘텐츠에 자바스크립트 핸들러나 이벤트를 붙이는 역할 +- 정적으로 생성된 HTML에 이벤트와 핸들러를 붙여 결과물을 만듦 +- render 함수와의 차이점은 hydrate는 기본적으로 이미 렌더링 된 HTML이 있다는 가정하에 작업을 수행하고 이 HTML을 기준으로 이벤트를 붙이는 작업만 실행 + +--- + +### Next.js + +- Next.js의 장점을 살리기 위해서 애플리케이션을 처음부터 서버에서 다시 불러와야 하는 드문 케이스 외에는 내부 페이지 이동시에 + - `` 대신 ``를 사용한다. + - window.location.push 대신 router.push를 사용한다. +- next/link로 이동하는 경우는 SSR이 아닌 클라이언트 라우팅/렌더링 방식으로 작동하기 때문 + +### 상태 관리는 왜 필요한가? + +React에서의 상태관리는 Context API를 떠올리기 쉬우나 Context API는 상태관리가 아니라 상태 주입을 도와주는 역할이다. 리액트의 상태 관리는 단방향으로 데이터 흐름을 변경하는 Flux 패턴의 리덕스를 시초로 발전되었다. + +--- + +- 웹에서 상태로 분류될 수 있는 것 + + - UI : 다크/라이트 모드, 라디오 input, 알림창의 노출 여부 + - URL : 브라우저에서 관리되고 있는 상태값 + - 폼 : loading, submit, disabled, vaildation 등 + - 서버에서 가져온 값 : API 요청 등 + +- 전역 상태 관리 라이브러리로 해결 된 것 + - prop 드릴링 문제를 해결 + - Context API와는 다르게 렌더링을 막아주는 기능 존재 + +--- + +## 05. 리액트와 상태 관리 라이브러리 + +### 상태 관리 라이브러리 + +- Recoil + + - Recoil의 상태값은 RecoilRoot로 생성된 Context의 스토어에 저장 + - 상태값에 접근할 수 있는 함수로 상태값을 변경하고 접근할 수 있다 + - 값의 변경이 발생하면 하위 컴포넌트에 전파 + +- Jotai + + - 작은 단위의 상태를 위로 전파할 수 있는 구조 + - 리액트 Context의 문제점인 불필요한 리렌더링을 해결하는 구조 + - 객체의 참조를 `WeakMap`에 보관해 해당 객체 자체가 변경되지 않는 한 별도의 키가 없이도 객체의 참조를 통해 값을 관리할 수 있다. + +- Zustand + - 하나의 스토어를 중앙 집중형으로 활용해 상태를 관리 + - 보일러 플레이트 코드가 적다. diff --git a/pages/4week/seolah/about.mdx b/pages/4week/seolah/about.mdx index df3be89..441e434 100644 --- a/pages/4week/seolah/about.mdx +++ b/pages/4week/seolah/about.mdx @@ -1 +1,7 @@ -## 이야기해보기 파일 +### p.278 + +hydrate API는 React 18로 오면서 hydrateRoot로 대체되었음 + +⇒ 18에서 사용하게 되면 17처럼 동작한다는 경고가 표시됨 + +![](./hydrate.png) diff --git a/pages/4week/seolah/flux.png b/pages/4week/seolah/flux.png new file mode 100644 index 0000000..a6bd498 Binary files /dev/null and b/pages/4week/seolah/flux.png differ diff --git a/pages/4week/seolah/hydrate.png b/pages/4week/seolah/hydrate.png new file mode 100644 index 0000000..abda623 Binary files /dev/null and b/pages/4week/seolah/hydrate.png differ diff --git a/pages/4week/seolah/study.mdx b/pages/4week/seolah/study.mdx index 1c8f9aa..cbedcab 100644 --- a/pages/4week/seolah/study.mdx +++ b/pages/4week/seolah/study.mdx @@ -1 +1,397 @@ -## 정리하기 파일 +# 04. 서버 사이드 렌더링 + +## 4.1 서버 사이드 렌더링이란? + +### 싱글 페이지 애플리케이션의 세상(SPA) + +> 렌더링과 라우팅에 필요한 대부분의 기능을 서버가 아닌 브라우저의 JS 에 의존하는 방식 + +- 서버에서 HTML 을 내려 받지 않고, 하나의 페이지에서 모든 작업을 처리하는 방식 +- 최초에 리소스할 JS가 크지만, 한번 로딩 된 이후에는 서버를 거쳐 필요한 리소스를 받아올 일이 적어, 훌륭한 UI/UX 제공 가능 + +**전통적인 방식의 어플리케이션과 싱글 페이지 어플리케이션의 작동 비교** + +- 전통적 방식: 페이지 전환 시 서버에서 다시 요청하여 새로 그리기에 부자연스러운 모습 +- 싱글 페이지: 최초 한번 리소스를 다운 받으면 페이지 전환 시 추가 다운로드 없이 페이지 전환이 일어나므로 더욱 매끄러움 (ex. Gmail 페이지) + +### 서버 사이드 렌더링(SSR)이란? + +**서버 사이드 렌더링의 장점** + +1. 최초 페이지 진입이 비교적 빠르다 + + - 사용자가 최초 페이지에 진입했을 때 페이지에 유의미한 정보가 그려지는 시간(First Contentful Paint)이 더 빨라질 수 있음 + + ⇒ 서버가 사용자에게 렌더링을 제공할 수 있을 정도의 충분한 리소스가 확보돼 있다는 일반적인 가정 하에 비교한 것 + +2. 검색 엔진과 SNS 공유 등 메타 데이터 제공이 쉽다 + - 검색 엔진에 제공할 정보를 서버에서 가공해서 HTML 답으로 제공할 수 있으므로 검색 엔진 최적화에 대응하기가 매우 용이 +3. 누적 레이아웃 이동이 적다 + + - **누적 레이아웃 이동이란?** + + 사용자에게 페이지를 보여준 이후 뒤늦게 어떤 HTML 정보가 추가되거나 삭제되어 마치 화면이 덜컥거리는 듯한 부정적인 UX + +4. 사용자의 디바이스 성능에 비교적 자유롭다 + - JS 리소스 실행은 사용자의 디바이스에서만 실행되므로 절대적으로 사용자 디바이스 성능에 의존적 + - SSR 수행 시 부담을 서버에 나눌 수 있음 +5. 보안에 좀 더 안전하다 + - 인증 혹은 민감한 작업을 서버에서 수행하고 결과만 브라우저에 제공해 보안 위협을 피할 수 있음 + +**서버 사이드 렌더링의 단점** + +1. 소스코드 작성 시 항상 서버를 고려해야 한다 + - `window` 객체 또는 `sessionStorage` 과 같이 브라우저에만 있는 객체 사용이 제한 + - 외부 의존 라이브러리 또한 서버에 대한 고려가 필요 +2. 적절한 서버가 구축돼 있어야 한다 +3. 서비스 지연에 따른 문제 + - SPA는 지연 시 ‘로딩 중’과 같은 작업이 진행중임을 안내 + - SSR은 지연 시 어떠한 정보도 제공이 불가하므로 부정적인 UX 제공 가능 + +### SPA와 SSR을 모두 알아야 하는 이유 + +**SSR 역시 만능이 아니다** + +- 잘못된 웹페이지 설계는 오히려 성능을 해칠 수 있음 + + ⇒ 서버와 클라이언트 두 군데 모두 관리 포인트만 늘어나는 역효과를 낳을 수 있음 + +- 웹페이지의 설계와 목적, 우선순위에 따라 SPA가 효과적일 수도 있음 + +**SPA 와 SSR 어플리케이션** + +1. 가장 뛰어난 SPA는 SSR로 생성되는 MPA(Multi Page Application) 보다 낫다 + + - 최초 렌더링 부분만 최적화하여 보여주고, 나머지는 게으른 로딩으로 렌더링 하도록 처리 + + ⇒ 뛰어난 성능과 매끄러운 사용자 경험 제공 가능 + +2. 평균적인 SPA는 MPA보다 느리다 + - 성능 최적화가 안된 SPA는 서버에서 빠르게 렌더링이 되는 MPA보다 느릴 가능성이 높음 + - MPA 라우팅으로 인한 문제를 해결하기 위한 API + - 페인트 홀딩(Paint Holding) : 같은 출처의 라우팅은 새로운 화면을 그릴 때 빈 화면이 아닌 이전 페이지의 모습을 잠깐 보여주는 방법 + - Back Forward Cache(BFCache) : 브라우저 앞, 뒤로 가기 실행 시 캐시에 저장된 페이지를 보여주는 기법 + - Shared Element Transitions : 페이지 라우팅 발생 시, 동일 요소는 콘텍스트를 유지하여 부드럽게 보여주는 기법 + +**현재의 SSR** + +- 최초 웹사이트 진입 시 SSR 방식으로 서버에 완성된 HTML을 제공받은 후, 라우팅에서 서버에서 내려받은 JS를 바탕으로 SPA처럼 작동 + +## 4.2 서버 사이드 렌더링을 위한 리액트 API 살펴보기 + +### [renderToString](https://react.dev/reference/react-dom/server/renderToString) + +> 리액트 컴포넌트를 랜더링해 HTML 문자열로 반환하는 함수로 가장 기초적인 SSR API + +```jsx +import { renderToString } from 'react-dom/server'; + +const html = renderToString(); +console.log(html); // For example, "..." +``` + +### [renderToStaticMarkup](https://react.dev/reference/react-dom/server/renderToStaticMarkup) + +> 리액트 컴포넌트를 HTML 로 만드는 renderToString 과 매우 유사한 함수 + +- 리액트에서만 사용하는 추가적인 속성(ex. `data-reactroot`)을 만들지 않음 + ⇒ HTML의 크기를 줄일 수 있음 +- **useEffect 와 같은 브라우저 API 사용이 불가능** + +```jsx +import { renderToStaticMarkup } from 'react-dom/server'; + +// The route handler syntax depends on your backend framework +app.use('/', (request, response) => { + const html = renderToStaticMarkup(); + response.send(html); +}); +``` + +### [renderToNodeStream](https://react.dev/reference/react-dom/server/renderToNodeStream) + +> `renderToString`과 동일한 결과물을 만들어내지만 두가지 차이가 존재 + +1. 브라우저에서 사용이 불가능 +2. 결과물의 타입이 string 이 아닌 Node.js의 `ReadableStream로` 만들어짐 + +- 대부분 널리 알려진 리액트 SSR 프레임워크는 해당 API 를 채택 + +### [renderToStaticNodeStream](https://react.dev/reference/react-dom/server/renderToStaticNodeStream) + +> `renderToNodeStream`의 결과물과 동일하나 리액트 JS 에 필요한 속성에 제공되지 않음 + +### [hydrate](https://react.dev/reference/react-dom/hydrate) + +> `renderToString`, `renderToNodeStream`로 생성된 HTML 컨텐츠에 JS 핸들러나 이벤트를 붙이는 역할 + +- 공식문서에 따르면 해당 API는 React의 주요 버전에서 제거될 예정이라 함 + ⇒ React 18에서 `hydrateRoot`로 대체 됨 + +## Next.js 톺아보기 + +### Next.js 란? + +> Vercel에서 만든 리액트 기반 풀스택 프레임 워크 + +### Next.js 시작하기 + +- `npx create-next-app@latest --ts` 명령어로 프로젝트 생성(ts 버전) + +### Data Fetching + +**getStaticPaths 와 getStaticProps** + +> 어떠한 페이지를 CMS(Contents Management System)나 블로그, 게시판과 같이 사용자와 관계없이 정적으로 결정된 페이지를 보여주고자 할 때 사용되는 함수 + +- 정적인 데이터만 제공하는 블로그 글, 약관 등을 빠르게 제공하는데 사용이 가능하다 + +```tsx +import { GetStaticPaths, GetStaticProps } from 'next'; + +export const getStaticPaths: GetStaticPaths = async () => { + return { + paths: [{ params: { id: '1' } }, { params: { id: '2' } }], + fallback: false, + }; +}; + +export const getStaticProps: GetStaticProps = async ({ params }) => { + const { id } = params; + const post = await fetchPost(id); + return { + props: { post }, + }; +}; + +export default function Post({ post }: { post: Post }) { + // post로 페이지를 렌더링한다. +} +``` + +**getServerSideProps** + +> 서버에서 실행되는 함수로 무조건 페이지 진입 전에 함수를 실행 + +- 응답값에 따라 페이지의 루트 컴포넌트에 props를 반환할 수도, 다른 페이지로 리다이렉트 시킬 수 있음 +- 사용자가 매 페이지를 호출할때마다 실행되고 이 실행이 끝나기 전까지는 사용자에게 어떠한 HTML도 보여줄 수 없음 + +```tsx +export const getServerSideProps: GetServerSideProps = async (context) => { + const { + query: { id = '' }, + } = context const post = await fetchPost(id.toString()); + + if (!post) { + redirect: { + destination: '/404' + } + } + + return { + props: { post }, + } +} + +``` + +**getInitialProps** + +> `getStaticProps`, `getServerSideProps`가 나오지 전의 유일한 수단 + +- 대부분의 경우에는 `getStaticProps`, `getServerSideProps` 를 사용을 권장 + +### 스타일 적용하기 + +**전역 스타일** + +- `\_app.tsx` 에 적용 + +**컴포넌트 레벨 CSS** + +- [name].moudule.css 같은 명명 규칙만 준수하기 + +**SCSS 와 SASS** + +- 기존과 동일하게 사용 가능 + +**CSS-in-JS** + +- JS 내부에 스타일시트를 삽입하는 방법 + +### next.config.js 살펴보기 + +**bathPath** + +- 기본 주소에 원하는 주소를 추가하는 기능 + +**swcMinifiy** + +- swc를 이용해 코드 압축 여부 설정 + +**poweredByHeader** + +- 응답 헤더에 next 관련 헤더를 넣을지 말지 결정하는 옵션 +- 보안 관련해서는 끄는 것을 추천 + +**redirects** + +- 특정 주소를 다른 주소로 보내고 싶을 때 사용, 정규식 지원 + +**reactStrictMode** + +- 리액트에서 제공하는 엄격 모드 설정 여부 + +**assetPrefix** + +- 빌드 결과물을 동일한 호스트가 아닌 다른 CDN에 업로드하고자 할 때 해당 부분에 CDN 주소 명시 + +# 05. 리액트와 상태관리 라이브러리 + +## 5.1 상태 관리는 왜 필요한가? + +- 상태는 어떠한 의미를 지닌 값이면 애플리케이션의 시나리오에 따라 지속적으로 변경될 수 있는 값 +- UI : 상호 작용이 가능한 모든 요소의 현재 값 +- URL : 브라우저에 의해 관리되고 있는 상태 값 +- Form: 폼의 상태, 로딩 / 제출 / 접근 가능여부 / 값의 유효성 +- 서버에서 가져온 값 + +### 리액트 상태 관리의 역사 + +**Flux 패턴의 등장** + +- 웹 애플리케이션이 방대해지고 데이터도 많아짐에 따라 어디서 상태가 변했는지 등을 추적하고 이해하는데 어려움 발생 +- 해당 문제의 원인을 양방향 데이터 바인딩이라 보고, 단방향으로 데이터 흐름을 변경하는 것을 제안 + + ⇒ Flux 패턴 + + ![alt text](./flux.png) + +- 사용자의 입력에 따라 데이터를 갱신하고 화면을 업데이트 하는 코드도 추가가 되는 불편함이 존재 + +**시장 지배자 리덕스의 등장** + +- Flux 구조에 Elm 아키텍쳐를 도입 +- Elm이란 데이터를 Model, View, Update 라는 단방향 흐름으로 강제하여 어플리케이션의 상태를 안정적으로 관리 +- 보일러플레이트가 많다는 단점이 존재 + +**Context API와 useContext** + +- props를 가지고 있는 부모에서 필요한 자식까지 끊임없는 컴포넌트의 인수로 넘겨야하는 불편함이 발생(props drilling) +- 이를 해결을 위해 전역 상태를 하위 컴포넌트에 주입할 수 있는 Context API 를 출시 + +**훅의 탄생, 그리고 React Query와 SWR** + +- 16.8 버전에서 함수 컴포넌트에 사용 가능한 훅 API 를 추가 +- 외부에서 데이터를 불러오는 `fetch`를 관리하는데 특화된 라이브러리 +- API에 대한 상태를 관리하고 있기에, HTTP 요청에 특화된 상태 관리 라이브러리라 볼 수 있음 + +**Recoil, Zustand, Jotai, Valtio에 이르기까지** + +- 개발자가 원하는 만큼의 상태를 지역적으로 관리 가능하게 만듦 +- 훅을 지원하여 함수형 컴포넌트에서 손쉽게 사용할 수 있다는 장점 존재 + +## 5.2 리액트 훅으로 시작하는 상태 관리 + +### 가장 기본적인 방법: useState와 useReducer + +- `useState`와 `useReducer`를 사용하면 간단한 상태 관리가 가능 +- 훅을 사용할 때 마다 컴포넌트 별로 초기화되므로 컴포넌트 별로 다른 상태를 가지게 되어, 해당 컴포넌트에서만 상태가 유효하다는 한계점이 존재 +- 만들기에 따라 재사용할 수 있는 지역 상태를 만들어주나, 지역 상태라는 한계로 인해 여러 컴포넌트에 걸쳐 공유하기 위해서는 컴포넌트 트리를 재설계 해야 함 + +```jsx +function Counter1({ counter, inc }: { counter: number, inc: () => void }) { + return ( + <> +

Counter1: {counter}

+ + + ); +} + +function Counter2({ counter, inc }: { counter: number, inc: () => void }) { + return ( + <> +

Counter2: {counter}

+ + + ); +} + +function Parent() { + const { counter, inc } = useCounter(); + + return ( + <> + + + + ); +} +``` + +### 지역 상태의 한계를 벗어나보자: useState의 상태를 바깥으로 분리하기 + +- 리액트 외부에서 관리되는 값에 대한 변경을 추적하고, 이를 렌더링하기 + + ⇒ 페이스북 팀에서 만든 `useSubscription` + +```tsx +function NewCounter() { + const subscription = useMemo( + () => ({ + // 스토어의 모든 값으로 설정해 뒀지만 selector 예제와 마찬가지로 + // 특정한 값에서만 가져오는 것도 가능하다. + getCurrentValue: () => store.get(), + subscribe: (callback: () => void) => { + const unsubscribe = store.subscribe(callback); + return () => unsubscribe(); + }, + }), + [] + ); + const value = useSubscription(subscription); + + return <>{JSON.stringify(value)}; +} +``` + +### useState와 Context를 동시에 사용해 보기 + +- 스토어 사용 시 하나의 스토어를 가지면 해당 스토어는 전역 변수처럼 작동하여 동일한 형태의 여러 개의 스토어를 가질 수 없음 +- `Context`를 사용하여 스토어를 컴포넌트에 주입하면 해결 가능함 + +### 상태관리 라이브러리 Recoil, Jotai, Zustand 알아보기 + +- Recoil과 Jotai는 `Context`와 `Provider`와 훅을 기반으로 작은 상태를 효율적으로 관리 가능 +- Zustand는 리덕스와 비슷하게 하나의 큰 스토어를 기반으로 상태를 관리하기 좋음 + + ⇒ `Context`가 아닌 스토어가 가지는 클로저를 기반으로 생성 + +**페이스북이 만든 상태 관리 라이브러리 Recoil** + +- 훅의 개념으로 상태 관리를 시작한 최초의 라이브러리 +- 최초 상태 개념인 `Atom`을 처음 선보임 + + ⇒ but 베타 + +- 특징 + - 리덕스와 달리 `redux-saga`나 `redux-thunk`를 사용하지 않아도 비동기 작업을 수월하게 처리 가능 + +**Recoil 에서 영감을 받은, 그러나 조금 더 유연한 Jotail** + +- Recoil의 `atom`모델에 영감을 받아 만들어진 상태관리 라이브러리 +- 하나의 큰 상태를 애플리케이션에 내려주는 것이 아닌 작은 단위의 상태를 위로 전파할 수 있는 구조 +- 불필요한 리렌더링을 해결하고자 설계되어있으며, 메모이제이션이나 최적화를 거치지 않아도 리렌더링이 발생하지 않도록 설계되어있음 +- 특징 + - Recoil의 atom 개념을 도입하며 API가 간결함 + - `selector` 없이 atom만으로 atom 값에서 또 다른 파생된 값 생성 가능 + +**작고 빠르면 확장에도 유연한 Zustand** + +- Redux에서 영감을 받아 만들어짐 +- 하나의 스토어를 중앙 집중형으로 활용하여 해당 스토어 내부에서 상태를 관리 +- 특징 + - 간결한 구조로 리액트 환경에서도 스토어를 생성하고 사용하기 쉬움 + - 크기도 작아 초보자들이 보기에 부담이 적음 + - Redux와 마찬가지로 미들웨어를 지원하여 `sessionStorage`에 추가로 저장하는 등의 기본적인 상태 관리 작동 외 추가적인 작업 정의 가능 diff --git a/pages/4week/xenosign/about.mdx b/pages/4week/xenosign/about.mdx index df3be89..ddbedd1 100644 --- a/pages/4week/xenosign/about.mdx +++ b/pages/4week/xenosign/about.mdx @@ -1 +1,47 @@ -## 이야기해보기 파일 +# 04. 서버 사이드 렌더링 + +## 4.1 서버 사이드 렌더링이란? + +### 4.1.3 SPA 와 SSR 을 모두 알아야 하는 이유 + +- [p. 268] MPA 에서 라우팅으로 인해 발생하는 문제를 해결하기 위한 API 를 SPA 에서는 JS 와 CSS 의 도움을 받아서 상당한 노력을 통해 기울여야 한다고 하는데, 진짜 그런가요? +- 개인적으로 사용자 디바이스가 정말 안좋은게 아닌 이상에는 오히려 반대가 아닌가 싶습니다. 통신 상황이 안좋다면 오히려 SSR 이 더 안좋은 유저 경험을 준다고 이미 책에서 밝히고 있으며, 페인트 홀딩, BFCache, Shared Element Transitions 은 SPA 프레임 워크에서 신경을 오히려 안써도 되는 부분 아닌가 싶어서 의문이 남네요. 다들 어떻게 생각하시나요? + +## 4.3 Next.js 톺아보기 + +- 리액트도 아직 볼게 많은데, 굳이 SSR 과 Next 가 여기 나오는 이유는 좀 이해가 안가네요. 오히려 머리 속만 더 복잡해지는 느낌입니다 +- 아마도 책 완성 단계에서 워낙 Next 랑 SSR 이 핫해지다보니 급하게 넣은게 아닌가 하는 의심이... + +### 4.3.2 Next.js 시작하기 + +- Next 14 에서 앱 라우팅이 지원 되어 구조가 다 변경이 되어버려서 애매한 내용이었습니다 + +#### 페이지에서 getServerSideProps 를 제거하면 어떻게 될까? + +- getServerSideProps 도 안알려주고 이런걸 알려주면... Next 모르는 사람은 어쩌라는 것인지... + +* * * + +# 05. 리액트와 상태 관리 라이브러리 + +## 5.2 리액트 훅으로 시작하는 상태 관리 + +### 5.2.2 지역 상태의 한계를 벗어나보자: useState의 상태를 바깥으로 분리하기 + +- [p. 367] 이거 예제 코드 길게 잘 써놨는데.... 이걸 쓰나? 싶어서 조금 애매하다는 느낌이었는데, 어찌 생각하시나요? 리덕스에 커스텀 미들웨어 붙이는 일이 생기면, 참고용으로는 좋을 것 같았습니다. +- 그런데 막상 리덕스는 왜 안알려주는 걸까요? + +### 5.2.4 상태관리 라이브러리 Recoil, Jotai, Zustand 알아보기 + +#### 페이스북이 만든 상태 관리 라이브러리 Recoil +#### Recoil 에서 영감을 받은, 그러나 조금 더 유연한 Jotail + +- 리코일은 10보다 biggerThan 이 >= 10 이고 Jotai 는 왜 > 10 이죠? ㅋㅋㅋㅋㅋ +- [p. 383] 리코일 코드에서 key 는 또 above10State 고.... 오락가락 하시네여 + +#### 작고 빠르면 확장에도 유연한 Zustand + +- 전 redux 로 배워서 그런지 zustand 가 압도적으로 편해 보입니다! 다들 어찌 생각하시나요? +- 실제로 점유율도 Zustand 가 급성장하는 걸 봐서 앞으로 좀 사용을 해볼 예정입니다! + +![상태관리 점유율](./npm-trend.png) \ No newline at end of file diff --git a/pages/4week/xenosign/app-router.png b/pages/4week/xenosign/app-router.png new file mode 100644 index 0000000..6d8d1e4 Binary files /dev/null and b/pages/4week/xenosign/app-router.png differ diff --git a/pages/4week/xenosign/npm-trend.png b/pages/4week/xenosign/npm-trend.png new file mode 100644 index 0000000..1669b4f Binary files /dev/null and b/pages/4week/xenosign/npm-trend.png differ diff --git a/pages/4week/xenosign/study.mdx b/pages/4week/xenosign/study.mdx index 1c8f9aa..1deceec 100644 --- a/pages/4week/xenosign/study.mdx +++ b/pages/4week/xenosign/study.mdx @@ -1 +1,764 @@ -## 정리하기 파일 +# 04. 서버 사이드 렌더링 + +## 4.1 서버 사이드 렌더링이란? + +### 4.1.1 싱글 페이지 어플리케이션의 세상 + +#### SPA(Single Page Application) 란? + +- 렌더링과 라우티엥 필요한 대부분의 기능을 서버가 아닌 브라우저의 JS 에 의존하는 방식 +- 서버에서 HTML 을 내려 받지 않고, 하나의 페이지의 JS 에 의해 모두 작어이 처리 되는 방식 +- 초기에 큰 JS 파일을 다운 받아야 하지만, 한번 로딩 된 이후에는 사용자에게 훌륭한 UI/UX 를 제공할 수 있다 + +#### 전통적인 방싱의 어플리케이션과 싱글 페이지 어플리케이션의 작동 비교 + +- 전통적 방식의 어플리케이션은 화면 전환시 서버에서 다시 HTML 을 받아서 그리기 때문에 부자연스러운 모습을 보인다 +- 반면 SPA 는 최초 한번 리소스를 다운 받으면 페이지 전환 시 추가 다운로드 없이 페이지 전환이 일어나므로 깔끔한 모습을 보인다 (Ex, Gmail 페이지) + +#### 싱글 페이지 렌더링 방식의 유행과 JAM 스택의 등장 + +- 과거 PHP 나 JSP 기반의 웹 어플리케이션은 대부분 서버 사이드 렌더링으로 이루어졌으며, JS 는 사용자에게 추가적인 기능을 제공하는 보조적인 수단으로 사용이 되었다 +- 2010년경 Backbone.js, Angular.js, Knockout.js 등이 등장하면서 JS 로도 MV + @ 프레임워크를 구현이 가능해 짐 +- 결국 JS 의 기능이 커짐에 따라 다른 많은 것들을 신경 써야하는 서버 사이드 렌더링 방식이 아닌 JS 로만 구성 된 프레임워크로 작성 된 SPA 가 인기를 끌게 됨 +- JAM(JS, API, MarkUp) 스택이 점차 확산되어 LAMP(Linux, Apache, MySQL, PHP) 를 대체하기 시작 +- Node.js 의 고도화에 따라 백엔드 또는 API 도 JS 로 개발하는 MEAN, MERN 스택이 인기를 끔(MongoDB, Express, Angular, React, Node.js) + +#### 새로운 패어다임의 웹서비스를 향한 요구 + +- 웹페이지에서 요구되는 사항이 점점 더 커짐에 따라 JS 리소스의 크기가 커져 결국 웹페이지 로딩의 속도가 과거에 비해 차이가 없거나 느려지는 문제가 발생 +- 이를 해결하기 위해 등장한 방식이 SSR(SSR, Sever Side Rendering) 이다 + +### 4.1.2 서버 사이드 렌더링이란? + +#### 서버 사이드 렌더링의 장점 + +- 사용자가 최초 페이지에 진입 했을 때 페이지에 유의미한 정보(FCP, First Contentful Paint)가 그려지는 시간이 빠르다. + - SPA 의 경우 JS 다운로드 후, HTTP 통신 작업이 완료 된 이후 페이지를 그리게 되므로 느리다 + - SSR 의 경우 HTTP 통신을 백엔드에서 하는 것이 더 빠르기도 하며, HTML 을 그리는 작업도 서버에서 직접 미리 그려서 내려주기 때문에 속도에서 이점을 가진다 + - 단, 서버가 충분한 리소스를 확보 하였을 때 이야기이다 +- 검색 엔진(SEO, Serch Engine Ooptimization)과 SNS 공유 등 메타데이터 제공이 쉽다 + - 검색 엔진은 HTML 의 정적인 데이터를 분석하므로 SPA 의 JS 의 데이터는 읽을 수 없어, SEO 및 메타 데이터 제공에 약점을 가진다 +- 누적 레이아웃 이동이 적다 + - 사용자에게 FCP 를 보여준 이후 뒤늦게 어떤 HTML 정보가 추가 되거나, 삭제되어 화면이 갑자기 변하는 부정적 사용자 경험이 줄어든다. + - 기사 페이지에서 기사를 읽고 있는데, 위 배너가 갑자기 로딩되어 글이 아래로 덜컥 이동하는 현상 같은 것 + - 다만, SSR 을 이용한다고 해서 누적 레이아웃 이동으로 부터 완전히 자유로울 순 없다 +- 사용자의 디바이스 성능에 비교적 자유롭다 +- 보안에 좀 더 안전하다 + - 주요 로직이 서버에서 전부 작동되어 전달 되므로, 보안에 안정적이다 + +#### 서버 사이드 렌더링의 단점 + +- 소스코드 작성 시 항상 서버를 고려해야 한다 + - 기존 CSR에서 사용하던 window 객체 또는 sessionStorage 과 같이 브라우저에만 있는 객체 사용이 제한된다 + - 외부 의존 라이브러리 역시 서버에 대한 고려가 필요하다 +- 적절한 서버가 구축되어 있어야 한다 + - 사용자의 수, 요청에 따라 적절히 대응할 수 있는 서버를 구축 해야만 함 + - 서버 장애에 대한 대응과, 분산 처리등에 대해서도 신경을 써서 개발이 필요하다 +- 서비스 지연에 따른 문제 + - CSR 은 통신 지연이 일어나도 어떤 화면이라도 뜬 상태가 되지만, SSR 은 아무런 화면이 안뜨는 현상이 생길 수 있으므로 각별한 주의가 필요하다 + +### 4.1.3 SPA 와 SSR 을 모두 알아야 하는 이유 + +#### SSR 역시 만능이 아니다 + +- 서버에 무거운 작업을 모두 미루는 것이 능사가 아니다, 잘못된 설계로 인해서 성능 저하는 물론 관리를 두 곳 모두 해야하는 문제 역시 발생 가능하다 + +#### SPA 와 SSR 어플리케이션 + +- 가장 뛰어난 SPA 는 SSR 로 생성되는 MPA(Multi Page Application) 보다 낫다 + - 최초 렌더링 부분만 최적화하여 보여주고, 나머지는 게으른 로디응로 렌더링 하도록 처리 -> 뛰어난 성능과 매끄러운 사용자 경험 제공 가능 +- 평균적인 SPA 는 MPA 보다 느리다 + - 성능 최적화가 안된 SPA 는 서버에서 빠르게 렌더링이 되는 MPA 보다 느릴 가능성이 높다 + - MPA 라우팅으로 인한 문제를 해결하기 위한 API 들 + - 페인트 홀딩(Paint Holding) : 같은 출처의 라우팅은 새로운 화면을 그릴 때 빈 화면이 아닌 이전 페이지의 모습을 잠깐 보여주는 방법 + - Back Forward Cache(BFCache) : 브라우저 앞, 뒤로 가기 실행 시 캐시에 저장된 페이지를 보여주는 기법 + - Shared Element Transitions : 페이지 라우팅 발생 시, 동일 요소는 콘텍스트를 유지하여 부드럽게 보여주는 기법 + +\*\* [p. 268] MPA 에서 라우팅으로 인해 발생하는 문제를 해결하기 위한 API 를 SPA 에서는 JS 와 CSS 의 도움을 받아서 상당한 노력을 통해 기울여야 한다고 하는데, 진짜 그런가요? + +\*\* 개인적으로 사용자 디바이스가 정말 안좋은게 아닌 이상에는 오히려 반대가 아닌가 싶습니다. 통신 상황이 안좋다면 오히려 SSR 이 더 안좋은 유저 경험을 준다고 이미 책에서 밝히고 있으며, 페인트 홀딩, BFCache, Shared Element Transitions 은 SPA 프레임 워크에서 신경을 오히려 안써도 되는 부분 아닌가 싶어서 의문이 남네요. 다들 어떻게 생각하시나요? + +#### 현재의 서버사이드 헨더링 + +- 기존 LAMP 방식은 모든 렌더링을 서버에 의존했지만, 요즘은 최초 진입시에는 SSR 로 렌더링된 적은 리소스의 HTML 을 받아서 빠르게 화면을 보여주고 SEO 도 만족을 시켜준다. 그리고 사용자가 최초 진입 된 페이지를 보는 동안 받아진 JS 를 로딩하여 나머지 동작은 SPA 처럼 동작한다 + +### 4.1.4 정리 + +- 최근에는 SPA 와 SSR 의 장점을 전 부 알고, 좋은 사용자 경험을 위해 두 가지 방법을 모두 이해하고 필요에 맞게 사용하는 것이 중요하다. + +## 4.2 서버 사이드 렌더링을 위한 리액트 API 살펴보기 + +### 4.2.1 renderToString + +- 리액트 컴포넌트를 랜더링해서 HTML 문자열로 반환하는 함수로 가장 기초적인 SSR API 이다 + +```jsx +const result = ReactDOMServer.renderToString( + React.createElement('div', { id: 'root' }, ); +) +``` + +- 결과물은 아래와 같이 반환된다 + +```html +
+
hello
+
    +
  • apple
  • +
  • banana
  • +
  • peach
  • +
+
+``` + +- 빠르게 브라우저가 HTML 을 그릴 수 있도록 제공해 주는 것이 목적이기 때문에 해당 API 는 이벤트 핸들러와 같은 JS 는 포함이 안되는 것을 볼 수 있다 +- data-reactroot="" 속성을 통해 컴포넌트의 루트 엘리먼트가 무엇인지를 식별하여, 이후 JS 를 실행하기 위한 기반이 된다 + +### 4.2.2 renderToStaticMarkup + +- 리액트 컴포넌트를 HTML 로 만드는 renderToString 과 매우 유사한 함수 +- 단, 리액트에서만 사용하는 추가적인 DOM 속성을(data-reactroot="" 같은 것) 만들지 않아 크기를 약간 줄일 수 있는 장점이 있다 +- 따라서, useEffect 와 같은 브라우저 API 사용이 불가능하다 + +```html +
+
hello
+
    +
  • apple
  • +
  • banana
  • +
  • peach
  • +
+
+``` + +### 4.2.3 renderToNodeStream + +- renderToString 과 동일한 결과물을 만들어내지만 두가지 차이를 가진다 +- 브라우저에서 사용이 불가능 +- 결과물이 string 이 아닌 Node.js 에 의존하는 ReadableStream 로 만들어진다 +- 결과물이 스트림으로 들어오기 때문에 데이터가 클 경우 작은 Chunk 로 분할하여 가져온게 된다. 따라서 HTML 의 크기가 클경우 작은 Chunk 로 분리되어 작성되므로 이점을 가진다 +- 대부분 널리 알려진 리액트 SSR 프레임워크는 해당 API 를 채택 + +### 4.2.4 renderToStaticNodeStream + +- renderToNodeStream 의 결과물에서 renderToStaticMarkup 과 마찬가지로 리액트 JS 에 필요한 속성만 빼는 API + +### 4.2.5 hydrate + +- renderToString, renderToNodeStream 로 생성된 HTML 컨텐츠에 JS 핸들러나 이벤트를 붙이는 역할 +- 기본적으로 랜더링된 HTML 이 있다는 가정하에, 이벤트를 붙이는 작업을 실행한다 + +```jsx +import * as ReactDOM from 'react-dom' import App from './App'; + +// containerId를 가리키는 element는 서버에서 렌더링된 HTML의 특정 위치를 의미한다. +// 해당 element를 기준으로 리액트 이벤트 핸들러를 붙인다. +const element = document.getElementById(containerId); + +ReactDOM.hydrate(, element); +``` + +- 리액트 관련 정보가 없는 순수한 HTML 정보가 전달 될 경우? + +```html + + + React App + + + + +
+ + +``` + +```jsx +function App() { + return 안녕하세요.; +} +import * as ReactDOM from "react-dom"; +import App from "./App"; +const rootElement = document.getElementById("root"); + +// Warning: Expected server HTML to contain a matching in
. +// at span // at App ReactDOM.hydrate(, rootElement) +``` + +- span 요소가 있는 것을 가정하고 작동되는 hydrate 이므로 주석과 같은 경거 문구가 출력 +- 다만, 경고가 출력 될 뿐 실행은 되는데 위와 같은 불일치가 발생하면 hydrate 가 렌더링한 결과물을 기준으로 웹페이지를 그리기 때문이다 => 물론 잘못된 사용법이다 +- hydrate 가 아무리 빨리 끝나도 시간이 걸리므로 시간을 기록하는 기능등에는 불일치가 발생할 수 밖에 없다 +- 이러한 에러를 해결하기 위해서는 해당 요소에 suppressHydrationWarning 을 추가하여 경고를 제거 가능 + +```jsx +
{new Date().getTime()}
+``` + +### 4.2.6 서버 사이드 렌더링 예제 프로젝트 + +### 4.2.7 정리 + +- SSR 구현을 위해서는 서버에서 다뤄야 할 것들이 많아 복잡하다 +- 따라서, 리액트 팀에서도 적절한 프레임 워크 사용을 권한다 + +## 4.3 Next.js 톺아보기 + +\*\* 리액트도 아직 볼게 많은데, 굳이 SSR 과 Next 가 여기 나오는 이유는 좀 이해가 안가네요. 오히려 머리 속만 더 복잡해지는 느낌입니다 + +\*\* 아마도 책 완성 단계에서 워낙 Next 랑 SSR 이 핫해지다보니 급하게 넣은게 아닌가 하는 의심이... + +### 4.3.1 Next.js 란? + +- Vercel 에서 만든 리액트 기반 풀스택 프레임 워크 +- PHP 대용으로 사용되기 위해 개발을 했다고 언급할 정도로 서버 사이드 렌더링을 염두에 둔 프레임워크 +- 과거 리액트 내부에서 SSR 을 위해 개발하다 중지 되었던 react-page 의 방향성을 유지 + +### 4.3.2 Next.js 시작하기 + +- npx-create-next-app 명령어로 프로젝트 생서 + +#### next.config.js + +- Next 의 환경설정을 담당 + +```ts +/** @type {import('next').NextConfig} */ +const nextConfig = { + reactStrictMode: true, + swcMinifi: true, +}; +``` + +- /\*_ @type {import('next').NextConfig} _/ : 타입 스크립트의 타입 도움을 받기 위한 코드 +- reactStrictMode : 리액트의 엄격 모드, 잠재적인 문제를 개발자에게 알리기 위한 도구 +- swcMinify : 번들링과 컴파일을 빠르게 수행하기 위해 만든 SWC 를 사용하여, 코드 최소화 작업을 진행. Babel 의 대용이다. Rust 로 구성된 SWC 로 인하여 속도가 더 빠르다 + +#### pages/\_app.tsx + +- 전체 페이지의 시작점 + +\*\* v14 에서는 page.tsx 가 해당 역할을 한다 + +#### pages/\_document.tsx + +- HTML 를 초기화 하는 부분 +- SEO 에 필요한 정보나 Title 등을 담을 수 있다 +- crate-next-app 으로는 생성이 되지 않는다 + +\*\* v14 에서는 layout.tsx 가 해당 역할을 한다 + +#### pages/\_error.tsx + +- 클라이언트 또는 서버에서 발생하는 500 에러는 담담하기 위한 페이지 +- crate-next-app 으로는 생성이 되지 않는다 + +#### pages/404.tsx + +- 404 페이지를 정의하는 파일 +- 따로 정의하지 않으면, next 에서 기본 제공하는 페이지로 작동한다 + +#### pages/500.tsx + +- 서버에서 발생하는 에러를 핸들링하는 페이지 +- \_error.tsx 와 500.tsx 가 동시에 존재하면 500.tsx 가 우선하여 실행된다 + +#### pages/index.tsx + +- pages 폴더의 하위 폴더명을 주소 값으로 사용 가능, 해당 폴더의 index.tsx 파일이 해당 주소 값에 대응 +- 폴더 내부의 다른 파일명은 해당 주소의 하위 주소로 참조 가능 +- /pages/hello.tsx => localhost:3000/hello 대응 +- /pages/test/hello.tsx => localhost:3000/test/hello 대응 + +- v13 에서 App Router 의 적용으로 구조가 변경 되었다 +- 이제는 pages 폴더가 app 폴더로 대체 되었으며, index.tsx 의 역할을 page.tsx 가 대신한다 +- app/test/page.tsx => localhost:3000/test 대응 +- 또한 기존의 파일명이 바로 주소에 대응되는 구조는 지원하지 않으며, layout.tsx -> template.tsx 의 순서로 구조를 가진다 + +![폴더 구조](./app-router.png) + +#### 서버 라우팅과 클라이언트 라우팅의 차이 + +- Next 는 CSR 과 SSR 을 동시에 지원한다 +- 사용자에게 최선의 경험을 제공하기 위해 최초 페이지는 SSR 을 사용하고, 페이지 이동이나 변화는 CSR 의 장점을 사용한다 + +#### 페이지에서 getServerSideProps 를 제거하면 어떻게 될까? + +```tsx +export default function Hello() { + console.log(typeof window === "undefined" ? "서버" : "클라이언트"); + + return <>hello; +} + +// 만약 아래 부분을 제거하면? +export const getServerSideProps = () => { + return { + props: {}, + }; +}; +``` + +- getServerSideProps 가 제거되면 서버에서 실행이 필요 없는 페이지로 처리되어 빌드시에 별도로 페이지 빌드를 하지 않는다 + +\*\* getServerSideProps 도 안알려주고 이런걸 알려주면... next 모르는 사람은 어쩌라는 것인지... + +#### /pages/api/hello.ts + +- Next 는 서버이므로 간단한 형태의 백엔드 api 구성이 가능 +- BFF(Backend for Frontend) 형태로 활용하거나, 자체로 풀스택 어플리케이션 구축, CORS 이슈 우회등을 위해 사용이 가능하다 + +### 4.3.3 Data Fetching + +#### getStaticPaths 와 getStaticProps + +- 사용자, 통신에 관계 없이 정적으로 결정된 페이지를 보여주고자 할 때 사용되는 함수 + +```tsx +import { GetStaticPaths, GetStaticProps } from "next"; + +export const getStaticPaths: GetStaticPaths = async () => { + return { + paths: [{ params: { id: "1" } }, { params: { id: "2" } }], + fallback: false, + }; +}; + +export const getStaticProps: GetStaticProps = async ({ params }) => { + const { id } = params; + const post = await fetchPost(id); + return { + props: { post }, + }; +}; + +export default function Post({ post }: { post: Post }) { + // post로 페이지를 렌더링한다. +} +``` + +- /pages/post/[id] 가 접근 가능한 정적 페이지를 구성하는 코드 +- params 로 1, 2 만 정의되어 있으므로 /post/3 이 호출되면 404 페이지가 반환 +- 미리 서버에서 예상되는 요청에 맞는 HTML 페이지를 만들어 놓기 때문에 빠른 응답이 가능하다 +- 정적인 데이터만 제공하는 블로그 글, 약관 등을 빠르게 제공하는데 사용이 가능하다 + +#### getServerSideProps + +- 서버에서 실행되는 함수로 무조건 페이지 진입 전에 함수를 실행한다 +- props 로 전달이 가능한 값은 JSON 형태로 직렬화가 가능해야만 함. class 나 Date 는 전달 불가능 +- 서버에서만 실행 되므로 window, document 등 사용 불가 +- 브라우저와는 달리 자신의 호스트 유추가 불가능 하므로 완전한 주소로만 api 요청이 가능 (/api/test 와 같은 주소 사용 불가) +- 해당 함수가 실행이 완료 되기 전까지는 사용자에게 어떠한 HTML 도 보여줄 수 없다 +- 조건에 따라 다른 페이지로 보내고 싶다면 redirect 를 사용 가능 + +```tsx +export const getServerSideProps: GetServerSideProps = async (context) => { + const { + query: { id = '' }, + } = context const post = await fetchPost(id.toString()); + + if (!post) { + redirect: { + destination: '/404' + } + } + + return { + props: { post }, + } +} +``` + +#### getInitialProps + +- getStaticProps, getServerSideProps 가 나오지 전의 유일한 수단 +- 대부분의 경우에는 getStaticProps, getServerSideProps 를 사용을 권장 +- 해당 함수는 서버와 클라이언트 모두에서 사용이 가능하므로 이러한 특징을 반드시 감안하여 코드를 작성해야만 한다 +- 레거시에서 사용을 대비하여 알아둘 것 + +### 4.3.4 스타일 적용하기 + +#### 전역 스타일 + +- \_app.tsx 에 적용 +- v14 이후에는 app/layout.tsx 에 적용 + +#### 컴포넌트 레벨 CSS + +- [name].moudule.css 같은 명명 규칙만 준수하면 된다 + +#### SCSS 와 SASS + +- 기존과 동일하게 사용 가능 + +#### CSS-in-JS + +- JS 내부에 스타일시트를 삽입하는 방법 +- styled-jsx, styled-components, Emotion, Linaria 등등 +- styled-components 의 경우 HTML 에 스타일을 입히는 것이 아니라 CSSOM 트리에 직접 삽입하므로 속도면의 이점이 있다 +- next 와 swc 사용을 원하면 styled-jsx, styled-components, emotion 중 하나 쓸 것 +- 단, v14 에서는 지원이 잘 안된다는 이슈가 있음 + +### 4.3.5 \_app.tsx 응용하기 + +### 4.3.6 next.config.js 살펴보기 + +- bathPath : 기본 주소에 원하는 주소를 추가하는 기능 +- swcMinifiy : swc 를 이용해 코드 압축 여부 설정 +- poweredByHeader : 응답 헤더에 next 관련 헤더를 넣을지 말지 결정하는 옵션, 보안 관련해서는 끄는 것을 추천 +- redirects : 특정 주소를 다른 주소로 보내고 싶을 때 사용, 정규식 지원 +- reactStrictMode : 리액트에서 제공하는 엄격 모드 적용 여부 +- assetPrefix : 빌드 결과물을 호스트가 아닌 다른 CDN 에 업로드하고자 할 때 해당 부분에 CDN 주소 명시 필요 + +### 4.3.7 정리 + +* * * + +# 05. 리액트와 상태 관리 라이브러리 + +## 5.1 상태 관리는 왜 필요한가? + +- 상태는 어떠한 의미를 지닌 값이면 어플리케이션의 시나리오에 따라 지속적으로 변경될 수 있는 값을 의미한다 +- UI : 상호 작용이 가능한 모든 요소의 현재 값 +- URL : 브라우저에 의해 관리되고 있는 상태 값 +- Form: 폼의 상태, 로딩 / 제출 / 접근 가능여부 / 값의 유효성 +- 서버에서 가져온 값 + +### 5.1.1 리액트 상태 관리의 역사 + +#### Flux 패턴의 등장 + +- 리액트로 작성 된 어플리케이션의 크기가 방대해짐에 따라 상태를 관리 추적하는 것에 어려움이 발생 +- 페이스북 팀은 상태 관리 어려움의 원인을 양방향 데이터 바인딩이라고 보고, 단방향으로 데이터 흐름을 변경하는 것을 제안하는데 이것이 바로 Flux 패턴이다 +- Action -> Dispatcher -> Store -> View 의 방향 +- 이러한 단방향 흐름은 상태의 관리 및 추적에는 유리했지만, 사용자의 입력에 따라 데이터를 갱신하고 화면을 업데이트 하는 코드도 추가가 되는 불편함이 존재 + +#### 시장 지배자 리덕스의 등장 + +- 리덕스는 Flux 구조에 Elm 아키텍쳐를 도입하여 시장을 지배 +- Elm 은 데이터를 Model, View, Update 라는 단방향 흐름으로 강제하여 어플리케이션의 상태를 안정적으로 관리 +- 다만 해당 기능을 사용하기 위해 많은 보일러 플레이트 코드가 필요하다는 단점이 존재 + +#### Context API 와 useContext + +- Props Drilling 등의 문제 해결을 위해 16.3 버전에서 Context API 를 출시 + +#### 훅의 탄생, 그리고 React Query 와 SWR + +- 16.8 버전에서 함수 컴포넌트에 사용 가능한 훅 API 를 추가 +- 훅으로 인하여 state 관리가 단순화 되어 React Query 와 SWR 라는 통신 요청에 특화된 상태 관리 라이브러리가 탄생 +- 리덕스에 비해 보일러 플레이트 코드를 줄일 수 있는 장점이 존재 + +#### Recoil, Zustand, Jotai, Valtio 에 이르기까지 + +- 리덕스에 이어 훅을 이용하여 작은 크기의 상태를 효율적으로 관리하는 상태관리 라이브러리가 탄생 + +### 5.1.2 정리 + +## 5.2 리액트 훅으로 시작하는 상태 관리 + +### 5.2.1 가장 기본적인 방법: useState 와 useReducer + +- useState 와 useReducer 를 사용하면 간단한 상태 관리가 가능 +- 하지만 훅을 사용할 때 마다 컴포넌트 별로 초기화 되므로 컴포넌트 별로 다른 상태를 가지게 되어, 해당 컴포넌트에서만 상태가 유효하다는 한계점이 존재 +- 아래와 같이 상태를 한단계 끌어올리는 방법이 존재하지만, 상태를 자식에게 props 로 전달해야하는 불편함이 발생 + +```jsx +function Counter1({ counter, inc }: { counter: number, inc: () => void }) { + return ( + <> +

Counter1: {counter}

+ + + ); +} + +function Counter2({ counter, inc }: { counter: number, inc: () => void }) { + return ( + <> +

Counter2: {counter}

+ + + ); +} + +function Parent() { + const { counter, inc } = useCounter(); + return ( + <> + + + + ); +} +``` + +### 5.2.2 지역 상태의 한계를 벗어나보자: useState의 상태를 바깥으로 분리하기 + +- 상태를 클로저에 맡기는 것이 아니라, JS 실행 문맥 내에서 초기화가 가능하도록 분리해보기 + +```tsx +// counter.ts +export type State = { counter: number }; + +// 상태를 아예 컴포넌트 밖에 선언했다. 각 컴포넌트가 이 상태를 바라보게 할 것이다. +let state: State = { counter: 0 }; + +// getter +export function get(): State { + return state; +} + +// useState와 동일하게 구현하기 위해 게으른 초기화 함수나 값을 받을 수 있게 했다. +type Initializer = T extends any ? T | ((prev: T) => T) : never; + +// setter +export function set(nextState: Initializer) { + state = typeof nextState === "function" ? nextState(state) : nextState; +} + +// Counter +function Counter() { + const state = get(); + + function handleClick() { + set((prev: State) => ({ counter: prev.counter + 1 })); + } + + return ( + <> +

{state.counter}

+ + + ); +} +``` + +- 하지만 해당 코드는 상태값의 변경은 정상적으로 작동되나 컴포넌트가 리렌더링되지 않아 문제가 발생한다 +- 해당 문제를 해결하기 위해, 외부 state 값을 컴포넌트 내부의 useState 의 인수로 전달하는 방식으로 해결 + +```tsx +function Counter1() { + const [count, setScount] = useState(state); + + function handleClick() { + // 외부에서 선언한 set 함수 내부에서 다음 상태값을 연산한 다음, 그 값을 로컬 상태값에도 넣기 + set((prev: State) => { + const newState = { counter: prev.counter + 1 }; + // setCount 의 호출로 리렌더링 발생 + setCount(newState); + // return 으로 업데이트 된 상태값을 외부 상태에 반영 + return newState; + }); + } + + return ( + <> + {/* 컴포넌트 내부의 state 인 count 사용 */} +

{count.counter}

+ + + ); +} +``` + +- 위와 같은 코드는 작동은 하지만 외부에서 관리하는 state 를 다시 컴포넌트 내부에서 state 로 선언하여 사용하는 등의 문제가 발생한다 +- 또한, 액션이 발생한 컴포넌트는 리렌더링이 발생하지만 다른 컴포넌트에서 외부 상태값을 참조하고 있을 경우 해당 컴포넌트는 다시 렌더링이 발생하기 전까지는 변경된 상태값 적용이 안된다 +- 이를 해결하기 위해서는 별도의 기능을 하는 createStore, useStore 등의 코드를 만들어서 사용해야만 한다 +- 이와 비슷한 기능은 React 의 useSubscription 을 통해 구현되어 있다 + +\*\* [p. 367] 이거 코드 길게 잘 써놨는데.... 흐음 애매하네요 ㅎㅎㅎ +\*\* 막상 리덕스는 알알려주는.... + +### 5.2.3 useState 와 Context를 동시에 사용해 보기 + +- 스토어를 사용하는 경우 스토어가 여러개가 되면 컴포넌트에서 어떤 스토어를 사용해야하는지 판단하기 어려운 단점이 존재 +- Context 를 이용하여 스토어를 컴포넌트에 주입하면, 위의 문제를 해결할 수 있다 +- 대부분의 상태관리 라이브러리는 위와 같은 방식으로 구성되어 있으며, 아래의 장점을 가진다 + - useState, sueReducer 와 같이 지역적 사용이 아닌 글로벌 활용이 가능하다 + - 상태가 변경되면 참조하고 있는 모든 컴포넌트의 렌더링이 발생한다 + +### 5.2.4 상태관리 라이브러리 Recoil, Jotai, Zustand 알아보기 + +- Recoil, Jotai 는 Context 와 Provider 그리고 훗을 기반으로 작은 상태를 효율적으로 관리하기 좋음 +- Zustand 는 하나의 큰 스토어를 기반으로 상태를 관리하기 좋음. Context 가 아닌 클로저를 기반으로 스토어가 생성됨 + +#### 페이스북이 만든 상태 관리 라이브러리 Recoil + +- 리액트를 위한 Atomic 상태 관리 라이브러리, 하지만 아직 베타 +- 리액트의 v18 에 맞추어 동시성 렌더링, 서버 컴포넌트, Streaming SSR 지원 이후에 1.0.0 을 배포 예정 +- RecoilRoot 를 최상단에 선언에 하나의 스토어를 만들고 Atom 이라는 상태 단위를 스토어에 등록 +- Recoil 의 훅을 통해 상태 변화를 구독하고 값이 변하면 리렌더링을 통해 Atom 의 값을 참조 + +- 예시 코드 + +```jsx +import { + RecoilRoot, + atom, + selector, + useRecoilState, + useRecoilValue, +} from "recoil"; + +// Recoil 파트 +const counterState = atom({ + key: "counterState", + default: 0, +}); + +function Counter() { + const [, setCount] = useRecoilState(counterState); + + function handleBtnClick() { + setCount((count) => count + 1); + } + + return ( + <> + + + ); +} + +const isBiggerThen10 = selector({ + key: "above10State", + get: ({ get }) => { + console.log(get(counterState)); + return get(counterState) >= 10; + }, +}); + +function Count() { + const count = useRecoilValue(counterState); + const biggerThen10 = useRecoilValue(isBiggerThen10); + + return ( + <> +

{count}

+

10 보다 큰가? : {JSON.stringify(biggerThen10)}

+ + ); +} + +function App() { + return ( +
+ + + + +
+ ); +} + +export default App; +``` + +##### 특징 + +- 메타에서 만드는 만큼 리액트의 신기능들을 가장 잘 지원할 것으로 예상 +- 리덕스와 달리 redux-saga 나 redux-thunk 를 사용하지 않아도 비동기 작업을 지원 +- 아직 정식 버전이 아니므로 사용에 있어서 주의 필요 + +#### Recoil 에서 영감을 받은, 그러나 조금 더 유연한 Jotail + +- Recoil 과 마찬가지로 Atomic 스타일의 상태 관리 라이브러리이며 작은 단위의 상태를 위로 전파할 수 있는 구조 +- atom 을 통해 상태를 선언하면 해당 상태는 컴포넌트 외부에서도 사용이 가능하다. 따라서 RecoilRoot 로 하위 컴포넌트를 감쌀 필요가 없다 +- Recoil 대비 간결한 코드, 객체 참조를 통해 문자열 key 값 없이 상태값 관리 가능 +- 그외의 API 및 localStorage 연동, Next, React Native 등 다양한 기능을 지원 +- 정식 버전이므로 상용 어플리케이션에 적용 가능 + +- 예시 코드 + +```jsx +import { atom, useAtom, useAtomValue } from "jotai"; + +const counterState = atom(0); + +function Counter() { + const [, setCount] = useAtom(counterState); + + const handleBtnClick = () => setCount((count) => count + 1); + + return ( + <> + + + ); +} + +const isBiggerThen10 = atom((get) => get(counterState) >= 10); + +function Count() { + const count = useAtomValue(counterState); + const biggerThen10 = useAtomValue(isBiggerThen10); + + return ( + <> +

{count}

+

10 보다 큰가? : {JSON.stringify(biggerThen10)}

+ + ); +} + +export default function JotailComponent() { + return ( +
+

조타이

+ + +
+ ); +} +``` + +\*\* 리코일은 10보다 biggerThan 이 >= 10 이고 Jotai 는 왜 > 10 이죠? ㅋㅋㅋㅋㅋ +\*\* [p. 383] 리코일 코드에서 key 는 또 above10State 고.... 오락가락 하시네여 + +#### 작고 빠르면 확장에도 유연한 Zustand + +- Redux 에서 영감을 받아 만든 Flux 타입 상태 관리 라이브러리, 따라서 하나의 큰 스토어를 기반으로 상태를 관리한다 +- 코드가 제일 간결하고 사용이 쉽다 +- 라이브러리의 코드의 용량 자체도 작아서 작고 빠르다 +- API 가 간단한 구조를 가진다면 Zustand 는 좋은 선택지이다 + +- 예시 코드 + +```jsx +import { create } from "zustand"; + +const useCounterStore = create((set) => ({ + count: 1, + inc: () => set((state) => ({ count: state.count + 1 })), + dec: () => set((state) => ({ count: state.count - 1 })), +})); + +export default function ZustandComponent() { + const { count, inc, dec } = useCounterStore(); + + const biggerThan10 = String(count >= 10); + + return ( +
+

Zustand

+ + +

{count}

+

10 보다 큰가? : {biggerThan10}

+
+ ); +} +``` + +\*\* 전 redux 로 배워서 그런지 zustand 가 압도적으로 편해 보입니다! 다들 어찌 생각하시나요? + +### 5.2.5 정리 + +![상태관리 점유율](./npm-trend.png) + +- 상황에 맞는, 그리고 프레임워크의 변화에 따르게 대응하는 라이브러리를 선택하는 것이 유리하다! + +\*\* 근데 리덕스나 발티오 같은건 안가르쳐 주나요?? 그래도 쉐어 1위인데? + +\*\* Zustand 연습하자! + diff --git a/pages/4week/yeonsuu21/study.mdx b/pages/4week/yeonsuu21/study.mdx index 1c8f9aa..07dbf90 100644 --- a/pages/4week/yeonsuu21/study.mdx +++ b/pages/4week/yeonsuu21/study.mdx @@ -1 +1,94 @@ -## 정리하기 파일 +# 4주차 + +# 04.서버사이드 렌더링 + +**싱글 페이지 애플리케이션** : 렌더링과 라우팅에 필요한 대부분의 기능을 서버가 아닌 브라우저의 자바스크립트에 의존하는 방식 + +→ 더 편한 개발 경험 제공 , 더욱 간편하게 제작 가능 + +## **서버 사이드 렌더링** + +장점 + +- 최초 페이지 진입이 비교적 빠르다 +- 검색 엔진과 SNS 공유 등 메타데이터 제공이 쉽다 +- 누적 레01아웃 이동이 적다 +- 시용자의 디바01스 성능에 비교적 자유롭다 + +단점 + +- 소스코드를 작성할 때 항상 서버를 고려해야 한다 +- 적절한서버가구축돼있어야한다 +- 서비스지연에따른문제 + +**싱글 페이지 애플리케이션 vs 서버 사이드 렌더링 애플리케이션** + +- 가장 뛰어난 싱글 페이지 애플리케이션은 가장 뛰어난 멀티 페이지 애플리케이션보다 낫다. +- 평균적인 싱글 페이지 애플러케이선은 평균적인 멀티 페이지 애플리케이선보다 느리다. + +## 서버 사이드 렌더링을 위한 리액트 API + +**renderToString** : 인수로 넘겨받은 리액트 컴포넌트를 렌더링해 HTML 문자 열로반환하는함수 + +**renderToStaticMarkup**: 리액트 컴포넌트를 기준으 로 HTML 문자열을 만든다 But 루트 요소에 추가한 data- reactroot와 같은 리액트에서만 사용하는 추가적인 DOM 속성을 만들지 않는다 + +**renderToNodeStream**: 브라우저에서 사용하는 것이 완전히 불가능 , 완전히 Node.js 환경에 의존 , 결과물 Node.js의 ReadableStream→ string을 얻기 위해서는 추가적인 처리가 필요하다. + +**renderToStaticNodeStream** : 순수 HTML 결 과물이 필요할 때 사용하는 메서드 + +**hydrate** : renderToString과 renderToNodeStream으로 생성된 HTML 콘벤 츠에 자바스크립트 핸들러나 이벤트를 붙이는 역할 + +## Next.js 톺아보기 + +Next.js 는 vercel 이라는미국스타트업에서만든 리액트기반 서버사이드렌더링 프레임워크 + +실제 디렉터리 구조가 곧 URL로 변환됨 + +**pages/404.tsx** : 원하는 스타일의 404 페이지를 이곳에서 만들 수 있다. + +**/pages/api/hello.ts** : 서버의 API를 정의하는 폴더, /pages/api/hello.ts는 /api/hello로 호출할 수 있으며, 이 주소는 다른 pages 파일과 다르게 HTML 요청을 하는 게 아니라 단순히 서버 요청을 주고받게 된다. + +**getServerSideProps** : 서버에서 실행되는 함수이 며 해당 함수가 있다면 무조건 페이지 진입 전에 이 함수를 실행 , 응답값에 따라 페이지의 루트 컴포넌트에 props를 반환할 수도, 혹은 다른 페이지로 리다이렉트 , + +props로 내려줄 수 있는 값은 JSON으로 제공할 수 있는 값으로 제한 + +꼭 최초에 보여줘야 히는 데이터가 아니라면 getServerSideProps보다는 클라이언트에서 호출하 는 것이 더 유리함 + +**getlnitialProps** : 대부분의 경우에는 getStaticProps나 getServerSideProps를 사용하는 것을 권장하며,getInitialProps는 굉장히 제한적인 예시에서만 사용 + +# 05. 리액트와 상태관리라이브러리 + +- Flux 패턴의등장 : 양방향이 아닌 단방향으로 데이터 흐름을 변경히는 것을 제안 +- 리덕스의 등장: Elm 아키텍처를 도입 : Elm은 웹페이지를 선언적으로 작성하기 위한 언어 +- Context API와 useContext +- 혹의 탄생, 그리고 React Query와 SWR +- Recoil, Zustand, Jotai, Valtio에 이르기까지 + +## 리액트 훅으로 시작하는 상태 관리 + +**useState와 useReducer** + +→ 약간의 구현상의 차이만 있을 뿐,두 훅 모두 지역 상태 관리를 위해 만들어졌다 + +**지역 상태 태의 한계를 벗어나보자 : useState의 상태를 바깥으로 분리하기** + +**useState와 Context를 동시에 사용해 보기** + +**상태 관리 라01브러리 Recoil, Jotai, Zustand 살펴보기** + +- Recoil + - RecoilRoot를 애플리케이션의 최상단에 선언 + - useStoreRef로 ancestorStoreRef의 존재를 확인 + - notifyComponents는 store, 그리고 상태를 전파할 storeState를 인수로 받아 이 스토어를 사용하고 있는 하위의존성을모두검색한다음, 여기에있는컴포넌트들을 모두확인해 콜백을 실행 +- atom + - Recoil의 최소 상태 단위 + - atom은 key 값을 필수로 가지며, 이 키는 다른 atom과 구별하는 식별자가 되는 필수 값 + - useRecoilValue는 atom의 값을 읽어오는 훅 +- Jotai + - atom 개념이 존재 + - Jotai는 atom을 생성할 때 별도의 key를 넘겨주지 않아도 된다 + - useAtomValue , useAtom +- Zustand + - 하나의 하나의 스토어를 중앙 집중형으 로 활용해 이 스토어 내부에서 상태를 관리하고 있다. + - state 변수 : 스토어의 상태값을 담아두는 곳 + - partial과 replace로 나눠져 있다 diff --git a/pages/4week/yonghyun/about.mdx b/pages/4week/yonghyun/about.mdx index df3be89..a798703 100644 --- a/pages/4week/yonghyun/about.mdx +++ b/pages/4week/yonghyun/about.mdx @@ -1 +1,5 @@ ## 이야기해보기 파일 + +- 상태관리 라이브러리들을 봤을때 러닝커브나 보일러플레이트를 봤을때 context API나 Recoil이 확실히 Zustand보다 더 접근성면에선 쉬운것 같은데 Zustand 점유율이 상대적으로 더 빠르게 올라가고 있는 이유는 뭘까요? (다른 라이브러리들의 한계? redux의 익숙함?) + +- 아무 조건이나 제한없이 한가지 상태관리 라이브러리를 정해서 사용해야 한다면? diff --git a/pages/4week/yonghyun/study.mdx b/pages/4week/yonghyun/study.mdx index 1c8f9aa..af8184a 100644 --- a/pages/4week/yonghyun/study.mdx +++ b/pages/4week/yonghyun/study.mdx @@ -1 +1,773 @@ ## 정리하기 파일 + +# 1. 서버 사이드 렌더링이란? + +## 1. 싱글 페이지 애플리케이션의 세상 + +### 싱글 페이지 애플리케이션이란? + +- 렌더링과 라우팅에 필요한 대부분의 기능을 서버가 아닌 브라우저의 자바스크립트에 의존하는 방식 +- 최초에 첫 페이지에서 데이터를 모두 불러온 이후에는 페이지 전환을 위한 모든 작업이 자바스크립트와 브라우저의 `history.pushState`와 `history.replaceState`로 이뤄진다. + - 페이지를 불러온 이후에는 서버에서 HTML을 내려받지 않고 하나의 페이지에서 모든 작업을 처리 +- 장점: 한번 로딩된 이후에는 서버를 거쳐 필요한 리소스를 받아올 일이 적어지기 때문에 사용자에게 훌륭한 UI/UX를 제공한다. +- 단점: 최초에 로딩해야 할 자바스크립트 리소스가 커진다. + +### 전통적인 방식의 애플리케이션과 싱글 페이지 애플리케이션의 작동 비교 + +- 서버 사이드: 페이지 전환이 발생할 때마다 새롭게 페이지를 요청하고, HTML 페이지를 다운로드해 파싱하는 작업으 거친다. + - 페이지를 처음부터 새로 그려야 해서 일부 사용자는 페이지가 전환될 때 부자연스러운 모습을 보게 된다. +- 클라이언트 사이드: 최초에 한번 모든 리소스를 다운로드하고 나면 이후 페이지를 전환할 때 추가로 리소스를 다운로드하는 시간이 필요 없어진다. + - 경우에 따라 페이지 전체를 새로 렌더링하는 것이 아니라 페이지 전환에 필요한 일부 영역만 다시 그리게 되므로 훨씬 더 매끄러운 UI를 보여줄 수 있게 된다. + +### 싱글 페이지 렌더링 방식의 유행과 JAM 스택의 등장 + +- 자바스크립트가 서서히 다양한 작업을 수행하게 되면서 자바스크립트를 모듈화하는 방안이 점차 논의되기 시작 + → CommonJS와 AMD(Asynchronous Module Definition) +- 2010년경 자바스크립트 수준에서 MVx 프레임워크를 구현하기 시작(Backbone.js, AngularJS, Knockout.js 등의 등장) + - 자바스크립트 개발자들은 웹페이지의 모든 영역(렌더링 ~ 사용자 인터랙션)을 담당하면서 이를 모두 아우를 수 있는 방식인 싱글 페이지 렌더링이 인기를 얻게 됨. + - 브라우저 내부에서 작동하는 스크립트만 신경쓰면 된다. → 간편한 개발 경험 +- 기존 LAMP(Linux + Apache + MySQL + PHP) 스택 → 웹 애플리케이션의 확장성 ↓ +- JAM(JavaScript + API + Markup) 스택: 자바스크립트와 마크업을 미리 빌드해 두고 정적으로 사용자에게 제공 → 서버 확장성 문제에서 자유로워짐 + +### 새로운 패러다임의 웹서비스를 향한 요구 + +- 자바스크립트 코드의 규모가 점차 커지면서 자바스크립트 파싱을 위해 CPU를 소비하는 시간이 크게 증가 → 웹페이지 로딩 시간 ↑ +- 사용자의 기기와 인터넷 속도 등 웹 전반을 이루는 환경이 크게 개선됐음에도 실제 사용자들이 느끼는 웹 애플리케이션의 로딩 속도는 5년 전이나 지금이나 크게 차이가 없거나 오히려 더 느리다. +- 웹 애플리케이션 개발자라면 웹 서비스의 성능을 역행하는 추세에 책임감을 가질 필요가 있다. + +## 2. 서버 사이드 렌더링이란? + +- 최초에 사용자에게 보여줄 페이지를 서버에서 렌더링해 빠르게 사용자에게 화면을 제공하는 방식 +- 웹페이지가 점점 느려지는 상황에 대한 문제의식을 싱글 페이지 애플리케이션의 태생적인 한계에서 찾고, 이를 개선하고자 다시 서버 사이드 렌더링이 떠오르고 있다. +- 클라이언트 사이드 렌더링은 사용자 기기의 성능에 영향을 받지만 서버 사이드 렌더링은 비교적 안정적인 렌더링이 가능 + +### 서버 사이드 렌더링의 장점 + +#### 최초 페이지 진입이 비교적 빠르다. + +- 사용자가 최초 페이지에 진입했을 때 페이지에 유의미한 정보가 그려지는 시간(FCP, First Contentful Paint)이 더 빠르다. + - 일반적으로 서버에서 HTTP 요청을 수행하는 것이 더 빠르다. + - 서버에서 HTML을 문자열로 미리 그려서 내려주는 것이 클라이언트에서 기존 HTML에 삽입하는 것보다 더 빠르다. + +#### 검색 엔진과 SNS 공유 등 메타데이터 제공이 쉽다. + +- 검색 엔진이 사이트에서 필요한 정보를 가져가는 과정 + 1. 검색 엔진 로봇(머신)이 페이지에 진입한다. + 2. 페이지가 HTML 정보를 제공해 로봇이 이 HTML을 다운로드한다. 단, 다운로드만 하고 자바스크립트 코드는 실행하지 않는다. + - 로봇은 페이지를 보는 것이 아닌 페이지의 정적인 정보를 가져오는 것이 목적이므로 자바스크립트를 다운로드하거나 실행할 필요가 없다. + 3. 다운로드한 HTML 페이지 내부의 오픈 그래프(Open Graph)나 메타(meta) 태그 정보를 기반으로 페이지의 검색(공유) 정보를 가져오고 이를 바탕으로 검색 엔진에 저장한다. +- 검색 엔진에 제공할 정보를 서버에서 가공해서 HTML 응답으로 제공할 수 있으므로 검색 엔진 최적화에 대응하기가 매우 용이 + +#### 누적 레이아웃 이동이 적다. + +- 누적 레이아웃 이동(CLS, Cumulative Layout Shift): 사용자에게 페이지를 보여준 이후에 뒤늦게 어떤 HTML 정보가 추가되거나 삭제되어 마치 화면이 덜컥거리는 것과 같은 부정적인 사용자 경험 +- 서버 사이드 렌더링을 사용한다 해도 이러한 문제에서 완전히 자유롭지는 못하다. + - `useEffect` → Next.js에서는 React 클라이언트 훅을 사용하려면 클라이언트 컴포넌트로 변경해줘야 함.(`'use client'`) + - API 속도가 모두 달랐을 때, 최초 페이지 다운로드가 느려진다. → [스트림](https://nextjs.org/learn/dashboard-app/streaming) + +#### 사용자 디바이스 성능에 비교적 자유롭다. + +- 자바스크립트 리소스 실행은 사용자의 디바이스에서 실행되므로 절대적으로 사용자 디바이스 성능에 의존적 +- 서버 사이드 렌더링을 수행하면 이러한 부담을 서버에 나눌 수 있으므로 사용자의 디바이스 성능으로부터 조금 더 자유로워질 수 있다. + - 인터넷 속도 + - 사용자 방문 증가로 서버에 부담이 가중되는 경우 등의 경우를 고려해야 함 + +#### 보안에 좀 더 안전하다. + +- JAM 스택의 문제점은 애플리케이션의 모든 활동이 브라우저에 노출된다는 것 + - API 호출, 인증 등의 민감한 작업도 포함된다. +- 서버 사이드 렌더링의 경우 민감한 작업을 서버에서 수행하고 그 결과만 브라우저에 제공해 이러한 보안 위협을 피할 수 있다. + +### 단점 + +#### 소스코드를 작성할 때 항상 서버를 고려해야 한다. + +- 브라우저 전역 객체인 `window` 또는 `sessionStorage`와 같이 브라우저에만 있는 전역 객체 등이 서버 사이드에서 실행되지 않도록 처리해야 한다. (외부 라이브러리 포함) + +#### 적절한 서버가 구축돼 있어야 한다. + +- 사용자의 요청을 받아 렌더링을 수행할 서버가 필요하다. + - 사용자의 요청에 따라 적절하게 대응할 수 있는 물리적인 가용량 확보 + - 예기치 않은 장애 상황에 대응할 수 있는 복구 전략 + - 요청을 분산시키고, 프로세스가 예기치 못하게 다운될 때를 대비해 PM2와 같은 프로세스 매니저의 도움 필요 + +#### 서비스 지연에 따른 문제 + +- 애플리케이션의 규모가 커지고 작업이 복잡해지고, 이에 따라 다양한 요청에 얽혀있어 병목 현상이 심해진다면, 서버 사이드 렌더링이 더 안 좋은 사용자 경험을 제공할 수도 있다. + +## 3. SPA와 SSR을 모두 알아야 하는 이유 + +### 서버 사이드 렌더링 역시 만능이 아니다. + +### 싱글 페이지 애플리케이션과 서버 사이드 렌더링 애플리케이션 + +- 싱글 페이지 애플리케이션 + - 최초 페이지 진입 시에 보여줘야 할 정보만 최적화해 요청 및 렌더링 + - 이미지 등 중요성이 떨어지는 리소스는 lazyloading 처리 + - 코드 분할로 불필요한 자바스크립트 리소스의 다운로드 및 실행 방지 + - 라우팅 발생 시 변경이 필요한 HTML 영역만 교체해 사용자의 피로감 최소화 +- 멀티 페이지 애플리케이션 + - 페인트 홀딩(Paint Holding): 같은 출처에서 라우팅이 일어날 경우 화면을 잠깐 하얗게 띄우는 대신 이전 페이지의 모습을 잠깐 보여주는 기법 + - back forward cache(bfcache): 브라우저 앞으로 가기, 뒤로가기 실행 시 캐시된 페이지를 보여주는 기법 + - Shared Element Transitions: 페이지 라우팅이 일어났을 대 두 페이지에 동일 요소가 있다면 해당 콘텍스트를 유지해 부드럽게 전환되게 하는 기법 + +### 현대의 서버 사이드 렌더링 + +- 최초 웹사이트 진입 시에는 서버 사이드 렌더링 방식으로 서버에서 완성된 HTML을 제공받음 +- 이후 라우팅에서는 서버에서 내려받은 자바스크립트를 바탕으로 마치 싱글 페이지 애플리케이션처럼 작동 + +# 2. 서버 사이드 렌더링을 위한 리액트 API 살펴보기 + +## 1. `renderToString` + +- 서버 사이드 렌더링을 구현하는 데 가장 기초적인 API +- 리액트 컴포넌트를 렌더링해 HTML 문자열로 반환하는 함수 +- `useEffect`와 같은 훅과 `handleClick`과 같은 이벤트 핸들러는 결과물에 포함되지 않음 + - 서버 사이드 렌더링은 단순히 '최초 HTML 페이지를 빠르게 그려주는 데'에 목적이 있기 때문 + - 실제로 웹페이지가 사용자와 인터랙션할 준비가 되기 위해서는 이와 관련된 별도의 자바스크립트 코드를 모아 다운로드, 파싱, 실행하는 과정을 거쳐야 한다. +- `data-reactroot` 속성: 리액트 컴포넌트의 루트를 식별하는 기준점 + +## 2. `renderToStaticMarkup` + +- `renderToString`과 유사하지만, 리액트에서만 사용하는 추가적인 DOM 속성(`data-reactroot` 등)을 만들지 않는다. +- `hydrate`를 수행하지 않는다는 가정 하에 순수 HTML만 반환한다. → 정적인 내용만 필요한 경우에 사용 + +## 3. `renderToNodeStream` + +- `renderToString`과 결과물이 완전히 동일하지만, 브라우저에서 사용할 수 없다. → 완전히 Node.js 환경에 의존 +- `renderToString`의 결과물 타입은 `string`인 반면, `renderToNodeStream`의 결과물 타입은 Node.js의 `ReadableStream` + - `utf-8`로 인코딩된 바이트 스트림으로 Node.js 환경에서만 사용할 수 있다. → `string`을 얻기 위해서는 추가적인 처리가 필요 + - `ReadableStream` 자체는 브라우저에서도 사용할 수 있는 객체이지만, `ReadableStream`을 만드는 과정이 브라우저에서 불가능하게 구현돼 있음. +- 스트림: 큰 데이터를 다룰 때 데이터를 청크(chunk, 작은 단위)로 분할해 조금씩 가져오는 방식 + - `renderToString`이 생성하는 HTML 결과물의 크기가 작다면 상관 없지만, 매우 커진다면 Node.js가 실행되는 서버에 큰 부담이 될 수 있다. + - 대부분의 리액트 서버 사이드 렌더링 프레임워크는 모두 `renderToNodeStream`을 채택하고 있다. + +## 4. `renderToStaticNodeStream` + +- Node.js 환경의 `renderToStaticMarkup` + +## 5. `hydrate` + +- `renderToString`과 `renderToNodeStream`으로 생성된 HTML 콘텐츠에 자바스크립트 핸들러나 이벤트를 붙이는 역할 +- `render`는 클라이언트에서만 실행되는 렌더링과 이벤트 핸들러 추가 등 리액트를 기반으로 한 온전한 웹페이지를 만드는 데 필요한 모든 작업을 수행 +- `hydrate`는 이미 렌더링된 HTML이 있다는 가정하에 작업이 수행 → 이벤트를 붙이는 작업만 실행 + - `hydrate`가 수행한 렌더링 결과물 HTML과 인수로 넘겨받은 HTML을 비교해 불일치가 발생하면 `hydrate`가 렌더링한 기준으로 웹페이지를 그리게 된다. + → 정상적으로 웹페이지가 만들어지고 렌더링된다고 해도 올바른 사용법은 아니다. + - 사실상 서버와 클라이언트에서 두 번 렌더링을 하게 되므로, 서버 사이드 렌더링의 장점을 포기하는 것 +- 불가피하게 불일치가 발생할 수 있는 경우에는 해당 요소에 `suppressHydrationWarning`을 추가해 경고를 끌 수 있다. + → 경고를 끄는 것이지 문제가 해결된 것이 아니다. + - 필요한 곳에서만 제한적으로 사용해야 한다. + +# 3. Next.js 톺아보기 + +## 1. Next.js란? + +- 리액트 기반 서버 사이드 렌더링 프레임워크 +- 디렉터리 기반 라우팅 + +# 1. 상태 관리는 왜 필요한가? + +- 상태: 의미를 지닌 값, 애플리케이션의 시나리오에 따라 지속적으로 변경될 수 있는 값 + - UI, URL, 폼, 서버에서 가져온 값 등 + +## 1. 리액트 상태 관리의 역사 + +### Flux 패턴의 등장 + +- 웹 애플리케이션이 비대해지고 상태도 많아짐 → 상태 추적과 이해가 매우 어려워짐 + + - 페이스북 팀은 양방향 데이터 바인딩을 그 원인으로 보았고, + - 단방향으로 데이터 흐름을 변경하는 것을 제안 → Flux 패턴의 시작 + +- 장점: 데이터의 흐름이 액션이라는 한 방향으로 줄어들어 데이터의 흐름 추적과 코드 이해가 쉬움. +- 단점: 사용자의 입력에 따라 데이터를 갱신하고 화면을 어떻게 업데이트해야 하는지도 코드로 작성해야 함. + +### 시장 지배자 리덕스의 등장 + +- Flux 구조를 구현하기 위해 만들어진 라이브러리 + [Elm 아키텍처] + - Elm: 웹 페이지를 선언적으로 작성하기 위한 언어 + - 모델(`model`): 애플리케이션의 상태 + - 뷰(`view`): 모델을 표현하는 HTML + - 업데이트(`update`): 업데이트를 수정하는 방식 + +1. 하나의 글로벌 상태 객체를 스토어에 저장 → Props Drilling 문제 해결 +2. `reducer` 함수를 통해 객체를 업데이트하는 작업을 디스패치해 업데이트를 수행 + - 웹 애플리케이션 상태에 대한 **복사본**을 반환, 새롭게 만들어진 상태를 전파 + +- 하고하 자는 일에 비해 너무 많은 보일러플레이트 → [RTK](https://ko.redux.js.org/introduction/why-rtk-is-redux-today/)가 나오면서 간소화 + +### Context API와 useContext + +- 리덕스의 부담스러운 보일러플레이트 → 리액트 16.3에서 Context API 출시 +- 리액트 16.3 버전 이전의 `context`와 `getChildContext()` + - 상위 컴포넌트가 렌더링되면 `getChildContext()`가 호출되면서 `shouldComponentUpdate`가 항상 `true`를 반환해 불필요한 렌더링이 발생 + - 컴포넌트 결합도 ↑ +- Context API는 상태 관리가 아닌 **주입**을 도와주는 도구 + +### 훅의 탄생, 그리고 React Query와 SWR + +- 리액트 16.8에서 무상태 컴포넌트를 선언하기 위해서 제한적으로 사용됐던 함수형 컴포넌트에 사용할 수 있는 다양한 훅 API 출시 +- 훅과 state의 등장으로 새로운 방식의 상태 관리 등장 → React Query & SWR + - 외부에서 데이터를 불러오는 fetch 관리 특화 라이브러리 + +```tsx +import useSWR from "swr"; + +const fetcher = (url) => fetch(url).then((res) => res.json()); + +export default function App() { + const { data, error } = useSWR( + "https://api.github.com/repos/vercel/swr", + fetcher + ); + + if (error) return "An error has occured"; + if (!data) return "Loading..."; + + return ( +
+

{JSON.stringify(data)}

+
+ ); +} +``` + +- 다른 곳에서 동일한 키로 호출하면 재조회하는 것이 아니라 `useSWR`이 관리하고 있는 캐시의 값을 활용 + +### Recoil, Zustand, Jotai, Valtio에 이르기까지 + +```tsx +// Recoil +const counter = atom({ key: "count", default: 0 }); +const todoList = useRecoilValue(counter); + +// Jotai +const countAtom = atom(0); +const [count, setCount] = useAtom(countAtom); + +// Zustand +const useCounterStore = create((set) => ({ + count: 0, + increase: () => set((state) => ({ count: state.count + 1 })), +})); +const count = useCounterStore((state) => state.count); + +// Valtio +const state = proxy({ count: 0 }); +const snap = useSnapshot(state); +state.count++; +``` + +- 훅을 활용해 작은 크기의 상태를 효율적으로 관리 +- `peerDependencies` 리액트 16.8 버전 이상 +- 전역 상태 관리 패러다임에서 벗어나 개발자가 원하는 만큼의 상태를 지역적으로 관리하는 것이 가능해짐. + +  + +# 2. 리액트 훅으로 시작하는 상태 관리 + +## 1. 가장 기본적인 방법: useState와 useReducer + +- `useState`와 `useReducer`를 기반으로 하는 사용자 정의 훅 + - 컴포넌트별로 초기화되므로 컴포넌트에 따라 서로 다른 상태를 가질 수밖에 없다. (상태의 파편화) +- `useState`를 기반으로 한 상태를 **지역 상태**(local state)라고 하며, 이 지역 상태는 해당 컴포넌트 내에서만 유효하다. +- 지역 상태를 여러 컴포넌트가 동시에 사용할 수 있는 전역 상태(global state)로 만들어 컴포넌트가 사용하는 모든 훅이 동일한 값을 참조할 수 있게 하려면 상태를 컴포넌트 밖으로 한 단계 끌어올리면 된다. + - 여러 컴포넌트가 동일한 상태를 사용할 수 있게 됐지만, props 형태로 필요한 컴포넌트에 제공해야 한다는 점은 여전히 불편하다. +- 재사용할 수 있는 지역 상태를 만들어 주지만 지역 상태라는 한계 때문에 여러 컴포넌트에 걸쳐 공유하기 위해서는 컴포넌트 트리를 재설계하는 등의 수고로움이 필요하다. + +## 2. 지역 상태의 한계를 벗어나보자: useState의 상태를 바깥으로 분리하기 + +- `useState`는 리액트가 만든 클로저 내부에서 관리되어 지역 상태로 생성되기 때문에 해당 컴포넌트에서만 사용할 수 있다. +- 상태를 리액트 클로저가 아닌 다른 자바스크립트 실행 문맥에서 초기화돼서 관리된다면? + + ```tsx + export type State = { counter: number }; + + let state: State = { counter: 0 }; + + export function get(): State { + return state; + } + + type Initializer = T extends any ? T | ((prev: T) => T) : never; + + export function set(nextState: Initializer) { + state = typeof nextState === "function" ? nextState(state) : nextState; + } + + function Counter() { + const state = get(); + + function handleClick() { + set((prev: State) => ({ counter: prev.counter + 1 })); + } + + return ( + <> +

{state.counter}

+ + + ); + } + ``` + + - state는 정상적으로 작동하지만, 리렌더링이 되지 않는다. (리액트의 렌더링 방식) + - `useState`, `useReducer`의 반환값 중 두 번째 인수가 호출돼야 한다. + - 부모 컴포넌트가 리렌더링되거나 함수가 재실행돼야 한다. + +- `useState`를 추가한다면? + + ```tsx + export type State = { counter: number }; + + let state: State = { counter: 0 }; + + export function get(): State { + return state; + } + + type Initializer = T extends any ? T | ((prev: T) => T) : never; + + export function set(nextState: Initializer) { + state = typeof nextState === "function" ? nextState(state) : nextState; + } + + function Counter1() { + const [count, setCount] = useState(state); + + function handleClick() { + set((prev: State) => { + const newState = { counter: prev.counter + 1 }; + setCount(newState); + return newState; + }); + } + + return ( + <> +

{state.counter}

+ + + ); + } + + function Counter2() { + const [count, setCount] = useState(state); + + function handleClick() { + set((prev: State) => { + const newState = { counter: prev.counter + 1 }; + setCount(newState); + return newState; + }); + } + + return ( + <> +

{state.counter}

+ + + ); + } + ``` + + - 외부의 상태도 수정하고 `useState`의 두 번째 인수도 실행하기 대문에 리액트 컴포넌트는 렌더링된다. + - 외부에 상태가 있음에도 함수형 컴포넌트의 렌더링을 위해 함수의 내부에 동일한 상태를 관리하는 `useState`가 존재하는 구조 + - 같은 상태를 바라보는 두 컴포넌트가 동시에 리렌더링되지 않는 문제 + - `useState`는 해당 컴포넌트 자체에서만 유효한 전략 + - 다른 컴포넌트에서는 상태의 변화에 따른 리렌더링을 일으킬 무언가가 없음. + +- 함수 외부에서 상태를 참조하고 렌더링까지 자연스럽게 일어나기 위한 조건 + 1. 컴포넌트 외부 어딘가에 상태를 두고 여러 컴포넌트가 같이 쓸 수 있어야 한다. + 2. 외부에 있는 상태를 사용하는 컴포넌트는 상태의 변화를 알아챌 수 있어야 하고 상태가 변화될 때마다 리렌더링이 일어나서 컴포넌트는 최신 상태값 기준으로 렌더링해야 한다. 상태 감지는 상태를 변경시키는 컴포넌트뿐만 아니라 이 상태를 참조하는 모든 컴포넌트에서 동일하게 작동해야 한다. + 3. 상태가 원시값이 아닌 객체인 경우에 그 객체에 내가 감지하지 않는 값이 변한다 하더라도 리렌더링이 발생해서는 안된다. +- 새로운 상태 관리 코드 + + - `store`의 값이 변경될 때마다 변경됐음을 알리는 `callback` 함수 + - `callback`을 등록할 수 있는 `subscribe` 함수 + + ```tsx + export type State = { counter: number }; + + type Initializer = T extends any ? T | ((prev: T) => T) : never; + + type Store = { + get: () => State; + set: (action: Initializer) => State; + subscribe: (callback: () => void) => () => void; + }; + + export const createStore = ( + initialState: Initializer + ): Store => { + // 인수 또는 게으른 초기화 함수로 store의 기본값 초기화 + let state = + typeof initialState !== "function" ? initialState : initialState(); + + const callbacks = new Set<() => void>(); + // get을 함수로 만들어 매번 최신값을 가져올 수 있게 만든다. + const get = () => state; + // set을 만들어 새로운 값을 넣을 수 있도록 만든다. + const set = (nextState: State | ((prev: State) => State)) => { + state = + typeof nextState === "function" + ? (nextState as (prev: State) => State)(state) + : nextState; + + // 값을 설정한 이후 등록된 모든 콜백을 실행해 렌더링을 유도한다. + callbacks.forEach((callback) => callback()); + + return state; + }; + + const subscribe = (callback: () => void) => { + callbacks.add(callback); + + // callback이 무한히 추가되는 것을 방지 (useEffect의 클린업 함수와 동일한 역할) + return () => { + callbacks.delete(callback); + }; + }; + + return { get, set, subscribe }; + }; + ``` + + - `createStore`는 자신이 관리해야 하는 상태를 내부 변수로 가진 다음, `get` 함수로 해당 변수의 최신값을 제공하며, `set` 함수로 내부 변수를 최신화하며, 이 과정에서 등록된 콜백을 모조리 실행하는 구조 + +- `createStore`로 만들어진 `store`의 값을 참조하고, 값의 변화에 따라 컴포넌트 렌더링을 유도할 사용자 정의 훅 + + ```tsx + // 훅의 인수로 사용할 store를 받음. + export const useStore = (store: Store) => { + // 컴포넌트 렌더링 유도를 위한 useState + const [state, setState] = useState(() => store.get()); + + // store의 값이 변경될 때마다 state의 값이 변경되는 것을 보장 + useEffect(() => { + const unsubscribe = store.subscribe(() => { + setState(store.get()); + }); + // 클린업 함수로 unsubscribe를 등록 + return unsubscribe; + }, [store]); + + return [state, store.set] as const; + }; + ``` + + - 스토어의 구조가 원시값이 아닌 객체인 경우, 현재는 `store`의 값이 바뀌면 무조건 `useState`를 실행하므로 스토어에 어떤 값이 바뀌든지 간에 리렌더링이 일어난다. + +- 변경 감지가 필요한 값만 `setState`를 호출해 객체 상태에 대한 불필요한 렌더링 막기 + + ```tsx + export const useStoreSelector = < + State extends unknown, + Value extends unknown + >( + store: Store, + selector: (state: State) => Value + ) => { + const [state, setState] = useState(() => selector(store.get())); + + useEffect(() => { + const unsubscribe = store.subscribe(() => { + const value = selector(store.get()); + setState(value); + }); + + return unsubscribe; + }, [store, selector]); + + return state; + }; + ``` + + - 필요한 값만 `select`하고, 객체에서 변경된 값에 대해서만 수행하도록 수정 + +## 3. useState와 Context를 동시에 사용해 보기 + +- `useStore`와 `useStoreSelector` 훅과 스토어를 사용하는 구조는 하나의 스토어를 가지면 이 스토어가 마치 전역 변수처럼 작동하게 되어 동일한 형태의 여러 스토어를 가질 수 없게 된다. +- 서로 다른 스코프에서 스토어의 구조는 동일하되, 여러 개의 서로 다른 데이터를 공유해 사용하고 싶다면? + → `createStore`를 이용해 동일한 타입으로 스토어를 여러 개 만들면 될 것 같지만 이 방법은 완벽하지도 않고 매우 번거롭다. + - 해당 스토어가 필요할 때마다 반복적으로 스토어를 생성해야 한다. + - 훅은 스토어에 의존적인 1:1 관계를 맺고 있으므로 스토어를 만들 때마다 해당 스토어에 의존적인 `useStore`와 같은 훅을 동일한 개수로 생성해야 한다. + - 이 훅이 어느 스토어에서 사용 가능한지를 가늠하려면 오직 훅의 이름이나 스토어의 이름에 의지해야 한다. +- `Context`를 활용해 해당 스토어를 하위 컴포넌트에 주입한다면 컴포넌트에서는 자신이 주입된 스토어에 대해서만 접근할 수 있게 된다. + + ```tsx + export const CounterStoreContext = createContext>( + createStore({ count: 0, text: "hello" }) + ); + + export const CounterStoreProvider = ({ + initialState, + children, + }: PropsWithChildren<{ initialState: CounterStore }>) => { + const storeRef = useRef>(); + + if (!storeRef.current) { + storeRef.current = createStore(initialState); + } + + return ( + + {children} + + ); + }; + + export const useCounterContextSelector = ( + selector: (state: CounterState) => State + ) => { + const store = useContext(CounterStoreContext); + + const subscription = useSubscription( + useMemo( + () => ({ + getCurrentValue: () => selector(store.get()), + subscribe: store.subscribe, + }), + [store, selector] + ) + ); + + return [subscription, store.set] as const; + }; + + const ContextCounter = () => { + const id = useId(); + const [counter, setStore] = useCounterContextSelector( + useCallback((state: CounterStore) => state.count, []) + ); + + function handleClick() { + setStore((prev) => ({ ...prev, count: prev.count + 1 })); + } + + return ( +
+ {counter} +
+ ); + }; + + const ContextInput = () => { + const id = useId(); + const [text, setStore] = useCounterContextSelector( + useCallback((state: CounterStore) => state.text, []) + ); + + function handleChange(e: ChangeEvent) { + setStore((prev) => ({ ...prev, text: e.target.value })); + } + + return ( +
+ +
+ ); + }; + + export default function App() { + return ( + <> + {/* 0 */} + + {/* hi */} + + + {/* 10 */} + + {/* hello */} + + + {/* 20 */} + + {/* welcome */} + + + + + ); + } + ``` + + - `Context`는 가장 가까운 `Provider`를 참조한다. + - 스토어를 사용하는 컴포넌트는 해당 상태가 어느 스토어에서 온 상태인지 신경쓰지 않아도 된다. + - `Context`와 `Provider`를 관리하는 부모 컴포넌트 입장에서는 자신이 자식 컴포넌트에 따라 보여주고 싶은 데이터를 `Context`로 잘 격리하기만 하면 된다. + - 부모와 자식 컴포넌트의 책임과 역할을 명시적인 코드로 나눌 수 있다. + +## 4. 상태 관리 라이브러리 Recoil, Jotai, Zustand 살펴보기 + +### 페이스북이 만든 상태 관리 라이브러리 Recoil + +#### RecoilRoot + +- Recoil을 사용하기 위해서는 `RecoilRoot`를 애플리케이션 최상단에 선언해야 한다. +- Recoil의 상태값은 `RecoilRoot`로 생성된 `Context`의 스토어에 저장된다.(`useStoreRef`, `ancestorStoreRef`) +- 스토어의 상태값에 접근할 수 있는 함수들이 있으며, 이 함수를 활용해 상태값에 접근하거나 상태값을 변경할 수 있다. (`getNextStoreID()`, `getState`, `replaceState` 등) +- 값의 변경이 발생하면 이를 참조하고 잇는 하위 컴포넌트에 모두 알린다.(`notifyComponents`) + +#### atom + +- 상태를 나타내는 Recoil의 최소 상태 단위 + + ```tsx + type Statement = { + name: string; + amount: number; + }; + + const InitialStatements: Array = [ + { name: "과자", amount: -500 }, + { name: "용돈", amount: 10000 }, + { name: "네이버페이충전", amount: -5000 }, + ]; + + const statementsAtom = atom>({ + key: "statements", + default: InitialStatements, + }); + ``` + + - `atom`은 `key` 값을 필수로 가진다. → 다른 `atom`과 구별하는 식별자 + - `default`: `atom`의 초깃값 + +#### useRecoilValue + +- `atom`의 값을 읽어오는 훅 +- 외부의 값을 구독해 렌더링을 강제로 일으킨다. (`useEffect`를 통해 `recoilValue`가 변경됐을 때 `forceUpdate`를 호출) + +#### useRecoilState + +- `useState`와 유사하게 값을 가져오고, 변경할 수도 있는 훅 + +### Recoil에서 영감을 받은, 그러나 조금 더 유연한 Jotai + +- 상향식 접근법(bottom-up) +- 리액트 Context의 문제점인 불필요한 리렌더링을 해결하고자 설계 + - 개발자들이 메모이제이션이나 최적화를 거치지 않아도 리렌더링이 발생하지 않는다. + +#### atom + +- 최소 단위의 상태 +- Recoil과 달리, 파생된 상태를 만들 수도 있다. +- `atom`을 생성할 때 별도의 `key`를 넘겨주지 않아도 된다. → 단순히 `toString()`을 위한 용도로 한정 +- `config` 객체를 반환 + - `init`: 초깃값 + - `read`: 값을 가져오는 함수 + - `write`: 값을 설정하는 함수 + - **`atom`에 상태를 저장하고 있지 않음** + +#### useAtomValue + +- Recoil과는 다르게 컴포넌트 루트 레벨에서 `Context`가 존재하지 않아도 된다. +- `Provider` 별로 다른 `atom` 값을 관리할 수도 있다. +- `atom`은 `store`에 존재한다. + - `WeakMap`이라고 하는 자바스크립트에서 객체만을 키로 가질 수 있는 독특한 방식의 Map을 활용 + - `atom` 객체 그 자체를 키로 활용해 값을 저장 + +#### useAtom + +- `useState`와 동일한 형태의 배열을 반환 +- `setAtom` 내부의 `write` 함수는 스토어에서 해당 `atom`을 찾아 직접 값을 업데이트한다. +- 값을 변경한 이후에는 `listener` 함수를 실행해 값의 변화가 있음을 전파하고, 사용하는 쪽에서 리렌더링이 수행되게 한다. + +### 작고 빠르며 확장에도 유연한 Zustand + +- 리덕스에 영감을 받아 만들어진 라이브러리 +- 하나의 스토어를 중앙 집중형으로 활용 +- 미들웨어를 지원한다. + +#### Zustand의 바닐라 코드 + +```ts +const createStoreImpl: CreateStoreImpl = (createState) => { + type TState = ReturnType; + type Listener = (state: TState, prevState: TState) => void; + let state: TState; + const listeners: Set = new Set(); // Set 형태로 선언되어 추가와 삭제, 중복 관리가 용이하게끔 설계 + + const setState: StoreApi["setState"] = (partial, replace) => { + const nextState = + typeof partial === "function" + ? (partial as (state: TState) => TState)(state) + : partial; + if (!Object.is(nextState, state)) { + const previousState = state; + state = + replace ?? (typeof nextState !== "object" || nextState === null) + ? (nextState as TState) + : Object.assign({}, state, nextState); + listeners.forEach((listener) => listener(state, previousState)); + } + }; + + const getState: StoreApi["getState"] = () => state; // 클로저의 최신 값을 가져오기 위해 함수로 만들어져 있다. + + const subscribe: StoreApi["subscribe"] = (listener) => { + listeners.add(listener); // listener 등록 + // Unsubscribe + return () => listeners.delete(listener); + }; + + const destroy: StoreApi["destroy"] = () => { + // listeners 초기화 + listeners.clear(); + }; + + const api = { setState, getState, getInitialState, subscribe, destroy }; +}; +``` + +- `state` 값을 `useState` 외부에서 관리 +- `partial`과 `replace`로 구분 + - `partial`: `state`의 일부분만 변경하고 싶을 때 사용 + - `replace`: `state`를 완전히 새로운 값으로 변경하고 싶을 때 사용 + +#### Zustand의 리액트 코드 + +```tsx +export function useStore( + api: WithReact>, + selector: (state: TState) => StateSlice = identity as any, + equalityFn?: (a: StateSlice, b: StateSlice) => boolean +) { + const slice = useSyncExternalStoreWithSelector( + api.subscribe, + api.getState, + api.getServerState || api.getInitialState, + selector, + equalityFn + ); + useDebugValue(slice); + return slice; +} +``` + +- `useSyncExternalStoreWithSelector`를 사용해서 `useStore`의 `subscribe`와 `getState`, 스토어에서 원하는 state를 고르는 함수인 `selector`를 넘겨준다. + +```tsx +const createImpl = (createState: StateCreator) => { + const api = + typeof createState === "function" ? createStore(createState) : createState; + + const useBoundStore: any = (selector?: any, equalityFn?: any) => + useStore(api, selector, equalityFn); + + Object.assign(useBoundStore, api); + + return useBoundStore; +}; + +export const create = ((createState: StateCreator | undefined) => + createState ? createImpl(createState) : createImpl) as Create; + +export default ((createState: any) => { + return create(createState); +}) as Create; +``` + +- 리액트 컴포넌트에서 해당 스토어를 즉시 사용할 수 있도록 `useStore`가 사용되었다. +- `useBoundStore`에 `api`를 `Object.assign`으로 복사해 `api`를 동일하게 사용할 수 있게 제공 diff --git a/pages/5week/0uizi0/about.mdx b/pages/5week/0uizi0/about.mdx index df3be89..d9f2f8d 100644 --- a/pages/5week/0uizi0/about.mdx +++ b/pages/5week/0uizi0/about.mdx @@ -1 +1,10 @@ ## 이야기해보기 파일 + +### 6장. 리액트 개발 도구로 디버깅하기 + +- 기명 함수의 사용을 권장하는데, 리팩토링 시에 특히 도움이 많이 될 것 같다고 생각함 + +### 7장. 크롬 개발자 도구를 활용한 애플리케이션 분석 + +- 개발 공부를 하면서, 개발자 도구를 제대로 활용을 못하고 있다고 생각해서 개인적으로 아쉬웠는데 책에서 간단하게 설명해줘서 좋았음. +- 서버에 직접 트래픽을 발생시켜서 메모리 누수를 확인하는 부분이 있었는데 신기했음 diff --git a/pages/5week/0uizi0/sampling.png b/pages/5week/0uizi0/sampling.png new file mode 100644 index 0000000..b0ffa05 Binary files /dev/null and b/pages/5week/0uizi0/sampling.png differ diff --git a/pages/5week/0uizi0/snapshot.png b/pages/5week/0uizi0/snapshot.png new file mode 100644 index 0000000..42a9ce1 Binary files /dev/null and b/pages/5week/0uizi0/snapshot.png differ diff --git a/pages/5week/0uizi0/study.mdx b/pages/5week/0uizi0/study.mdx index 1c8f9aa..ef917d3 100644 --- a/pages/5week/0uizi0/study.mdx +++ b/pages/5week/0uizi0/study.mdx @@ -1 +1,117 @@ -## 정리하기 파일 +## 6장. 리액트 개발 도구로 디버깅하기 + +### 컴포넌트 개발 도구 + +**Components** + +- 정적인 현재 리액트 컴포넌트 트리의 내용을 디버깅하기 위한 도구 +- **16.8 버전 이하 :** 함수 선언식 또는 표현식으로 선언되지 않은 컴포넌트는 **명칭 추론 불가능**을 이유로 문제가 발생 +- **18.6 버전 이후 :** 16.8 버전에서 ‘일부 명칭을 추론할 수 없는 Anymous’가 \_cs, \_c5 등으로 개선됨. + 그러나, 임의로 명칭이 선언되었기 때문에, **컴포넌트를 기명 함수로 변경**하는 방법을 지향한다. +- 컴포넌트를 익명 함수로 선언하기 곤란한 경우, 혹은 함수명과는 별개로 특별한 명칭을 부여해 명시적으로 확인이 필요한 경우 displayName을 사용하면 좋다. + 그러나 이는 빌드 도구가 사용하지 않는 코드로 인식해 삭제할 가능성이 있기 때문에, 개발 모드에서만 제한적으로 참고하는 것이 좋다. + +**Profiler** + +- 리액트가 렌더링하는 과정에서 발생하는 상황을 확인하기 위한 도구 +- **프로파일링 메뉴 :** 리액트가 렌더링할 대 어떤 일이 벌어지는지 확인할 수 있는 도구 +- **Flamegraph :** 랜더 커밋별로 어떠한 작업이 일어났는지 나타내는 도구. 너비가 넓을수록 해당 컴포넌트를 렌더링하는 데 오래 걸렸다는 것을 의미 +- **Ranked :** 해당 커밋에서 렌더링하는 데 오랜 시간이 걸린 컴포넌트를 순서대로 나열한 그래프 +- **Timeline :** 시간이 지남에 따라 컴포넌트에서 어떤 일이 일어났는지를 확인할 수 있는 도구. 시간의 흐름에 따라 리액트가 작동하는 내용을 추적하는데 유용 + +## 7장. 크롬 개발자 도구를 활용한 애플리케이션 분석 + +### 네트워크 탭 + +- 불필요한 요청 또는 중복되는 요청이 없는지 +- 웹페이지 구성에 필요한 리소스 크기가 너무 크지 않은지 +- 리소스를 불러오는 속도는 적절한지 또는 너무 속도가 오래 걸리는 리소스는 없는지 +- 리소스가 올바른 우선순위로 다운로드되어 페이지를 자연스럽게 만들어가는지 + +### 메모리 탭 + +- 애플리케이션에서 발생하는 메모리 누수, 속도 저하, 혹은 웹페이지 프리징 현상을 확인할 수 있는 유용한 도구 + +**힙 스냅샷** + +![힙 스냅샷](./snapshot.png) + +- 해당 시점의 메모리 내용만 촬영하는 프로파일링 기법 +- 현재 시점의 메모리 상황을 알고 싶을 때 + +> **# 얕은 크기와 유지된 크기** +> +> - 얕은 크기 : 객체 자체가 보유하는 메모리 바이트의 크기 +> - 유지된 크기 : 해당 객체 자체뿐만 아니라 다른 부모가 존재하지 않는 모든 자식 객체들의 크기까지 더한 값 +> +> ```jsx +> var counter = 0 +> var instances = [] +> +> func Y() { +> this.j = 5 +> } +> +> func X() { +> this.i = counter++ +> this.y = new Y() +> } +> +> export default func App() { +> func handleClick() { +> instances.push(new X()) +> } +> +> return +> } +> ``` +> +> 스냅샷을 통해 X는 유지된 크기가 100, 얕은 크기가 52로 선언됨 ( Y를 제외하고 X라는 객체 자체의 크기는 52 / Y 객체의 크기는 48) +> +> ⇒ 메모리 누수를 찾을 때는 얕은 크기(객체 자체의 크기)는 작으나 유지된 크기(객체가 참조하고 있는 모든 객체들의 크기)가 큰 객체를 찾아야 함. +> +> ⇒ 해당 객체가 복잡한 참조 관계를 가지고 있다는 뜻. 이러한 객체가 오랜 시간 동안 메모리에 남아있다면 그로 인해 많은 메모리를 점유하고 있을 수 있음 + +- useMemo나 useCallback과 같은 의존성이 있는 값들이 정말로 렌더링 사이에 그대로 유지되는지 육안으로 직접 확인 가능 (스냅샷 사이에 해당 훅으로 감싼 값들이 재생성되지 않았기 때문) + +**타임라인 할당 계측** + +![타임라인 할당 계측](./timeline.png) + +- 시간의 흐름에 따라 메모리의 변화를 살펴보고 싶을 때 +- 메모리 변화를 일으킨 변수가 무엇인지, 그리고 해당 변수가 어느 정도 크기를 차지하고 있는지 등을 확인 +- 특정 변수를 클릭 해 **전역 변수로 저장**을 누르면 해당 변수가 무슨 값을 가지고 있는 객체인지 확인 가능 + +**할당 샘플링** + +![할당 샘플링](./sampling.png) + +- 메모리 공간을 차지하고 있는 자바스크립트 함수를 보고 싶을 때 +- 할당 계측과의 유사점 : 시간의 흐름에 따라 발생하는 메모리 점유를 확인할 수 있다 +- 할당 계측과의 차이점 : 자바스크립트 실행 스택별로 분석할 수 있고, 이 분석을 함수 단위로 한다 +- 프로파일링할 때 브라우저에 주는 부담을 최소화할 수 있어 장시간에 걸쳐 디버깅을 수행해야 할 때 유리 + +⇒ 즉, 아래와 같은 경우엔 할당 샘플링을 활용하는 것이 좋다 + +- 메모리 누수가 짐작되지만 정확히 어디에서 발생하는지 확인하기 어려워 힙 스냅샷을 촬용해 비교하기 어려운 경우, +- 오랜 기간 메모리 누수가 의심되어 프로파일링을 장기간 수행해야 하는 경우 + +### Next.js 환경 디버깅하기 + +**디버그 모드 실행** + +```jsx +"dev" : NODE_OPTIONS= ’ --inspect ’ next dev +``` + +**Next.js 서버에 트래픽 유입시키기** + +- 사용자가 서서히 유입되면서 메모리 누수가 발생하는 경우가 많음 +- 따라서 서버에 직접 트래픽을 발생시켜서 확인하는 편이 제일 확실한 방법 +- 아래 코드를 터미널에서 실행시켜, 사용자의 트래픽이 서버로 몰리는 상황을 시뮬레이션해 볼 수 있음 + +```jsx +>> ab -k -c 50 -n 10000 "http://127.0.0.1:3000/" +// http://127.0.0.1:3000/을 향해 한 번에 50개의 요청을 총 10,000회 시도한다 +// ab : 단순히 요청을 수행하는 것뿐만 아니라 요청으로부터 응답받는 데 걸린 시간, 바이트 크기 등 다양한 정보 확인 가능 +``` diff --git a/pages/5week/0uizi0/timeline.png b/pages/5week/0uizi0/timeline.png new file mode 100644 index 0000000..d725287 Binary files /dev/null and b/pages/5week/0uizi0/timeline.png differ diff --git a/pages/5week/doyeon/about.mdx b/pages/5week/doyeon/about.mdx index df3be89..5aa103f 100644 --- a/pages/5week/doyeon/about.mdx +++ b/pages/5week/doyeon/about.mdx @@ -1 +1,18 @@ ## 이야기해보기 파일 + +[p. 119] +> 그럼에도 이러한 임의로 선언된 명칭으로는 개발 도구에서 컴포넌트를 특정하기는 어렵다. 이러한 문제를 해결하기 위해 컴포넌트를 기명 함수로 변경할 수 있다. +> 만약 함수를 기명 함수로 바꾸기 어렵다면 함수에 displayName 속성을 추가하는 방법도 있다. +고차 컴포넌트의 경우 이러한 기법을 유용하게 사용할 수 있다. +> +```jsx +const MemoizedComponent = memo(function () { + return <>MemoizedComponent +}) + +MemoizedComponent.displayName = '메모 컴포넌트입니다.' +``` + + +컴포넌트를 기명 함수로 선언하면 개발 도구에서 확인하는 데에는 도움을 주지만, 함수 선언을 위한 코드 몇줄이 추가되기 때문에 귀찮아질 수 있다고 생각한다. +기명 함수로 바꾸기 어려울 경우 함수에 displayName 속성을 추가하는 방법도 있다고 하는데 이건 처음 알게 된 사실이라 자주 쓰이는지 궁금해요. diff --git a/pages/5week/doyeon/study.mdx b/pages/5week/doyeon/study.mdx index 1c8f9aa..6984b4a 100644 --- a/pages/5week/doyeon/study.mdx +++ b/pages/5week/doyeon/study.mdx @@ -1 +1,151 @@ ## 정리하기 파일 + +# 06장. 리액트 개발 도구로 디버깅하기 + +## 6.1 리액트 개발 도구란? + +react-dev-tools 는 리액트로 만들어진 다양한 애플리케이션을 디버깅하기 위해 만들어졌으며, 리액트 웹 뿐만 아니라 리액트 네이트부 등 다양한 플랫폼에서 사용할 수 있다. +가장 편리한 방법은 브라우저 확장 프로그램을 사용하는 것이다. + +## 6.2 리액트 개발 도구 설치 + +브라우저 확장 도구로 설치하고 나면 리액트 로고의 색을 통해 구분할 수 있다. + +- 회색 : 리액트 개발 도구가 정상적으로 접근할 수 없는 페이지. 리액트로 개발되지 않은 페이지 +- 빨간색 : 개발 모드인 리액트 웹 애플리케이션에 정상적으로 리액트 개발 도구가 접근할 수 있다는 의미 +- 파란색 : 실제 프로덕션에 배포돼 있는 웹 애플리케이션을 방문할 경우 + +## 6.3 리액트 개발 도구 활용하기 + +### 6.3.1 컴포넌트 + +정적인 현재 리애트 컴포넌트 트리의 내용을 디버깅하기 위한 도구 + +**📍 컴포넌트 트리** + +components 탭에서는 현재 리액트 애플리케이션의 컴포넌트 트리를 확인할 수 있다. 컴포넌트의 구조뿐만 아니라 props와 내부 hooks 등 다양한 정보를 확인할 수 있다. + +기명 함수로 선언되어 있으면 해당 컴포넌트명을 보여주고, 익명 함수로 선언돼 있으면 Anonymous라는 이름으로 컴포넌트를 보여준다. + +16.9 버전 이후부터는 일부 Anonymous가 _c3, _c5 등으로 개선된 것을 확인할 수 있다. +그럼에도 이러한 임의로 선언된 명칭으로는 개발 도구에서 컴포넌트를 특정하기는 어렵다. 이러한 문제를 해결하기 위해 컴포넌트를 기명 함수로 변경할 수 있다. + +만약 함수를 기명 함수로 바꾸기 어렵다면 함수에 displayName 속성을 추가하는 방법도 있다. +고차 컴포넌트의 경우 이러한 기법을 유용하게 사용할 수 있다. + +```jsx +const MemoizedComponent = memo(function () { + return <>MemoizedComponent +}) + +MemoizedComponent.displayName = '메모 컴포넌트입니다.' +``` + +물론 빌드한 트리를 확인하는 경우 기명 함수로 선언한다 하더라도 압축 도구 등이 난수화하기 때문에 확인하기가 어려워진다. 그러므로 displayName 과 함수명은 개발 모드에서만 제한적으로 참고하는 것이 좋다. + +**컴포넌트명과 props** + +- 컴포넌트 명칭 : Anonymous (익명 함수) +- key : kidsValueProp +- 빨간색 경고 표시 : 해당 애플리케이션이 strict mode로 렌더링되지 않았다는 것을 의미 + + + +**컴포넌트 도구** + +- 첫 번째 눈 아이콘 : Element 탭으로 이동하며, 해당 컴포넌트가 렌더링한 HTML 요소가 선택된다. +- 두 번째 벌레 아이콘 : console 탭에 해당 컴포넌트의 정보가 기록된다. 해당 컴포넌트가 받는 props, 컴포넌트 내부에서 사용하는 hooks, 컴포넌트의 HTML 요소인 nodes가 기록된다. +- 세 번째 소스코드 아이콘 : 해당 컴포넌트의 소스코드를 확인할 수 있다. + +**컴포넌트 props** + +- 해당 컴포넌트가 받은 props를 확인할 수 있다. 원시값뿐만 아니라 함수도 포함돼 있다. + +**컴포넌트 hooks** + +- 컴포넌트에서 사용 중인 훅 정보를 확인할 수 있다. useState는 State와 같이 use가 생략된 이름으로 나타난다. + +**컴포넌트를 렌더링한 주체, rendered by** + +- rendered by는 해당 컴포넌트를 렌더링한 주체가 누구인지 확인할 수 있다. +- 프로덕션 모드에서는 react-dom의 버전만 확인할 수 있지만 개발 모드에서는 해당 컴포넌트를 렌더링한 부모 컴포넌트까지 확인할 수 있다. + +### 6.3.2 프로파일러 + +리액트가 렌더링하는 과정에서 발생하는 상황을 확인하기 위한 도구. + +- 렌더링되는 과정에서 어떤 컴포넌트가 렌더링됐는지 +- 또 몇 차례나 렌더링이 일어났으며 어떤 작업에서 오래 걸렸는지 + +프로덕션 빌드로 실행되는 리액트에서는 사용할 수 없다. + +**설정 변경하기** + +- General - Highlight updates when components render : 컴포넌트가 렌더링될 때마다 해당 컴포넌트에 하이라이트를 표시한다. 매우 유용 +- Debugging - Hide logs during second render in Strict Mode : 엄격 모드에서 실행되는 경우, 원활한 디버깅을 위해 useEffect 등이 두 번씩 작동하는데 이를 막아준다. +- Profiler - Record why each component rendered while profiling : 프로파일링 도중 무엇 때문에 컴포넌트가 렌더링 됐는지 기록한다. + +**Flamegraph** + +불꽃 모양의 아이콘 탭에서는 렌더 커밋별로 어떠한 작업이 일어났는지 나타낸다. 이 메뉴는 비단 컴포넌트가 렌더링이 얼마나 걸렸는지 확인할 수 있을뿐더러 렌더링되지 않은 컴포넌트에 대한 정보도 확인할 수 있다. + +**Ranked** + +해당 커밋에서 렌더링하는 데 오랜 시간이 걸린 컴포넌트를 순서대로 나열한 그래프다. Flamegraph와의 차이점은 모든 컴포넌트를 보여주는 것이 아니라 단순히 렌더링이 발생한 컴포넌트만 보여준다. + +**Timeline** + +시간이 지남에 따라 컴포넌트에서 어떤 일이 일어났는지를 확인할 수 있다. 리액트 18 이상의 환경에서만 확인할 수 있다. + + +# 07장. 크롬 개발자 도구를 활용한 애플리케이션 분석 + +자바스크립트 메모리, 네트워크, 소스, 실제 HTML 및 CSS 등 리액트가 아닌 일반적인 웹 애플리케이션 환경의 디버깅을 수행하려면 리액트 개발 도구가 아닌 브라우저 개발자 도구를 활용해야 한다. + +## 7.1 크롬 개발자 도구란? + +크롬 개발자 도구에서 웹사이트를 제대로 디버깅하고 싶다면 개인정보 보호 모드(시크릿 모드)에서 개발자 도구를 여는 것을 권장한다. 이유는 브라우저에 설치돼 있는 각종 확장 프로그램 때문이다. + +## 7.2 요소 탭 + +현재 웹페이지를 구성하고 있는 HTML, CSS 등의 정보를 확인할 수 있다. + +## 7.3 소스 탭 + +웹 애플리케이션을 불러오기 위해 실행하거나 참조된 모든 파일을 확인할 수 있다. + +스코프, 호출 스택 등 현재 자바스크립트가 실행되고 있는 구조도 확인할 수 있다. + +- 감시 : 감시하고 싶은 변수를 선언하고, 해당 변수의 정보를 확인할 수 있는 메뉴다. +- 중단점 : 현재 웹사이트에서 추가한 중단점을 확인할 수 있다. +- 범위 : 현재 중단점에서의 스코프를 의미한다. +- 호출 스택 : 현재 중단점의 콜스택을 확인할 수 있다. +- 전역 리스너 : 현재 전역 스코프에 추가된 리스너 목록을 확인할 수 있다. +- XHR/가져오기, DOM, 이벤트 리스너, CSP 위반 중단점 : 소스의 중단점 이외에 다양한 중단점을 확인할 수 있다. + +## 7.4 네트워크 탭 + +해당 웹페이지를 접속하는 순간부터 발생하는 모든 네트워크 관련 작동이 기록된다. 웹사이트에서 자주 사용하는 HTTP 요청부터 웹 소켓에 이르기까지, 웹페이지가 외부 데이터와 통신하는 정보를 확인할 수 있다. + +- 불필요한 요청 또는 중복되는 요청이 없는지 +- 웹페이지 구성에 필요한 리소스 크기가 너무 크지 않은지 +- 리소스를 불러오는 속도는 적절한지 / 너무 오래 걸리는 리소스는 없는지 +- 리소스가 올바른 우선순위로 다운로드되어 페이지를 자연스럽게 만들어가는지 + +## 7.5 메모리 탭 + +현재 웹페이지가 차지하고 있는 메모리 관련 정보를 확인할 수 있다. 애플리케이션에서 발생하는 메모리 누수, 속도 저하, 웹페이지 프리징 현상을 확인할 수 있는 유용한 도구다. + +**힙 스냅샷** + +- 현재 메모리 상황을 사진 찍듯이 촬영할 수 있다. +- 스냅샷을 활용할 때는 의심이 되는 지점을 먼저 추측해 본 뒤에 두 개 이상의 스냅샷을 보는 것이 훨씬 쉽다. + +**타임라인의 할당 계측** + +- 현재 시점의 메모리 상황이 아닌, 시간의 흐름에 따라 메모리 변화를 살펴보고 싶다면 타임라인의 할당 계측을 사용하면 된다. +- 주로 로딩이 되는 과정의 메모리 변화 또는 페이지에서 어떠한 상호작용을 했을 때 메모리의 변화 과정을 알고 싶을 때 사용한다. + +**할당 샘플링** + +- 메모리 공간을 차지하고 있는 자바스크립트 함수를 볼 수 있다. \ No newline at end of file diff --git a/pages/5week/jkyeun/about.mdx b/pages/5week/jkyeun/about.mdx index df3be89..b3be607 100644 --- a/pages/5week/jkyeun/about.mdx +++ b/pages/5week/jkyeun/about.mdx @@ -1 +1,13 @@ -## 이야기해보기 파일 +## 이야기해보기 + +- p.433 개발자들은 왜 다 MacOS를 쓸까 ?! (이번주 얘기할 게 하도 없어서 ..) + - 이 분 나이도 꽤 있어보이시고 해서 window를 쓰시지 않을까? 했는데 역시 Mac이네요! +- 오늘 내용은 확실히 클린업이나 리팩토링 같은 업무에서 도움이 될 것 같아요. (그럴 시간 없다는게 함정) +- p.415 훅에 넘겨주는 함수를 익명 함수 대신 기명 함수로 넘겨주면 해당 훅을 실행할 때 실행되는 함수의 이름을 확인할 수 있다. + - 보통 컨벤션처럼 익명함수를 쓰는 부분을 기명 함수를 쓰는 것에 대한 생각 + +## 번외 + +- 비즈니스적 성장과 기술적 성장 중 더 중요하게 생각하는 것은? -> 면접 질문이라면 ? +- 개발자로서의 커리어를 언제까지 이어나갈 것인가 개발자로서 생각하는 로드맵? +- 학사로 괜찮을까? diff --git a/pages/5week/jkyeun/study.mdx b/pages/5week/jkyeun/study.mdx index 1c8f9aa..61114e1 100644 --- a/pages/5week/jkyeun/study.mdx +++ b/pages/5week/jkyeun/study.mdx @@ -1 +1,57 @@ -## 정리하기 파일 +# [6장] 리액트 개발 도구로 디버깅하기 + +## 리액트 개발 도구란? + +- 리액트 팀은 리액트 애플리케이션의 원활한 개발을 위한 개발 도구인 react-dev-tools를 만들어 제공하고 있다. + +## 리액트 개발 도구 활용하기 + +### 컴포넌트 + +- Components 탭에서는 현재 리액트 애플리케이션의 컴포넌트 트리를 확인할 수 있다. +- props와 hooks 등 다양한 정보를 확인할 수 있다. +- 컴포넌트 트리 + - 리액트 애플리케이션 전체의 트리 구조를 한 눈에 보여준다. + - `tsMemoizedComponent.displayName = '메모 컴포넌트입니다.';` -> displayName을 설정하면 리액트 개발자 도구에서 컴포넌트명을 확인할 수 있다. +- 컴포넌트 도구 + - 눈 아이콘을 누르면 해당 컴포넌트가 HTML의 어디에서 렌더링됐는지 확인할 수 있다. + - 누르는 즉시 크롬 개발 도구의 메뉴 중 하나인 요소(Element) 탭으로 즉시 이동한다. + - 벌레 아이콘을 클릭하면 콘솔 탭에 해당 컴포넌트의 정보가 console.log()를 통해 기록된다. + - 소스코드 아이콘을 클릭하면 소스 코드를 확인할 수 있고, {} 버튼을 눌러 난독화된 코드를 볼 수 있도록 된다. +- 컴포넌트 hooks + - 훅에 넘겨주는 함수를 익명 함수 대신 기명 함수로 넘겨주면 해당 훅을 실행할 때 실행되는 함수의 이름을 확인할 수 있다. + - hooks도 props도 값을 더블클릭해 원하는 값으로 수정할 수 있다. + +### 프로파일러 + +- 컴포넌트 메뉴가 정적인 현재 리액트 컴포넌트 트리의 내용을 디버깅하기 위한 도구라면 프로파일러는 리액트가 렌더링하는 과정에서 발생하는 상황을 확인하기 위한 도구다. +- General 탭의 Highlight updates when components render: 컴포넌트가 렌더링될 때마다 해당 컴포넌트에 하이라이트를 표시한다. 이 기능은 매우 유용한 기능이므로 꼭 켜두는 것이 좋다. +- Profiler 탭의 Record why each component rendered while profiling: 프로파일링 도중 무엇 때문에 컴포넌트가 렌더링됐는지 기록한다. 애플리케이션 속도가 조금 느려질 수는 있지만 디버깅에 도움이 되는 옵션이므로 켜두는 것이 좋다. +- Flamegraph 탭에서는 렌더 커밋별로 어떠한 작업이 있어났는지 나타난다. + +### 타임라인 + +- Timeline에서는 시간이 지남에 따라 컴포넌트에서 어떤 일이 일어났는지를 확인할 수 있다. + +# [7장] 크롬 개발자 도구를 활용한 애플리케이션 분석 + +- 브라우저 환경에서 발생할 수 있는 문제를 디버깅할 수 있는 도구를 브라우저 개발자 도구라고 한다. +- 시크릿 모드 또는 프라이빗 모드라 불리는 개인정보 보호 모드에서 페이지와 개발자 도구를 여는 것을 권장한다. +- 소스 탭에서는 소스 중단점을 생성해 자바스크립트 실행을 중단시키고 디버깅을 수행할 수 있다. +- 소스 탭의 호출 스택에서는 해당 시점의 실행 콘텍스트가 어떻게 저장되어 어떤 모습을 하고 있는지 볼 수 있다. +- 네트워크 탭에서는 동일한 주소의 요청이 두 번 실행되는지 확인해볼 수 있다. +- 메모리 탭에서는 애플리케이션에서 발생하는 메모리 누수, 속도 저하, 혹은 웹페이지 프리징 현상을 확인할 수 있다. +- 타임라인 할당 계측은 시간에 흐름에 따라 메모리 변화를 확인할 수 있는 기능이다. +- 할당 샘플링은 이에 더해 자바스크립트 실행 스택별로 분석할 수 있다. + +## Next.js 환경 디버깅하기 + +- Next.js 프로젝트를 디버그 모드로 실행하기 + `"dev": NODE_OPTIONS='--inspect' next dev` +- ab를 이용하여 HTTP 서버의 성능을 벤치마킹하기 + `ab -k -c 50 -n 10000 "http://127.0.0.1:3000/"` +- 이러한 디버깅을 통해 메모리 성능 등을 확인할 수 있다. + +## 정리 + +- 꼭 리액트로 만들어진 웹페이지가 아니더라도 웹페이지의 작동 방식을 자세히 이해하고 싶다면 개발자 도구를 적극 활용하자. diff --git a/pages/5week/xenosign/study.mdx b/pages/5week/xenosign/study.mdx index 1c8f9aa..2f60f02 100644 --- a/pages/5week/xenosign/study.mdx +++ b/pages/5week/xenosign/study.mdx @@ -1 +1,136 @@ -## 정리하기 파일 +# 06. 리액트 개발 도구로 디버깅 하기 + +## 6.1 리액트 개발 도구란? + +- 리액트 팀에서 제공하는 개발 도구 react-dev-tools +- 브라우저 확장 프로그램으로 설치하여 사용 가능 + +## 6.2 리액트 개발 도구 설치 + +## 6.3 리액트 개발 도구 활용하기 + +### 6.3.1 컴포넌트 + +#### 컴포넌트 트리 + +- 어플리케이션의 전체 트리 구조를 한눈에 보여준다 +- 상단 입력란에서는 정규식을 활용하여 검색이 가능하다 +- 함수 선언식 또는 표현식으로 선언되지 않은 컴포넌트는 개발자 도구를 통해 컴포넌트의 이름을 확인할 수 없는 문제가 있다 + +- 아래의 코드로 실행하면 컴포넌트 명이 Anonymous, \_c 로 뜨게 된다 + +```jsx +const withSampleHOC = (Component) => { + return function () { + return ; + }; +}; + +const HOCComponent = withSampleHOC(() => <>HOCComponent); + +export default function DevTools() { + return ; +} +``` + +- 이를 수정하기 위해 기명 함수를 적용하면, 컴포넌트 명이 함수명으로 바로 나오기 때문에 추후 디버깅에 있어서 도움이 된다 + +```jsx +const withNamedSampleHOC = (Component) => { + return function withNamedSampleHOC() { + return ; + }; +}; + +const NamedHOCComponent = withNamedSampleHOC(function NamedHOCComponent() { + return <>NamedHOCComponent; +}); +``` + +- 혹은 displayName 을 설정하여 해결하는 방법도 있다. displayName 을 추가하면 개발자 도구에 해당 속성 이름으로 표기 + +```jsx +const MemoizedComponent = memo(() => <>MemoizedComponent); + +MemoizedComponent.displayName = "메모된 컴포넌트"; +``` + +#### 컴포넌트명과 props + +##### 컴포넌트명과 Key + +##### 컴포넌트 도구 + +##### 컴포넌트 props + +##### 컴포넌트 hooks + +##### 컴포넌트를 렌더링한 주체, rendered by + +- 해당 컴포넌트를 렌더링한 주체를 확인 가능 + +### 6.3.2 프로파일러 + +- 리액트 트리가 아닌 리액트가 랜더링하는 과정에서 발생하는 상황을 확인하기 위한 도구 + +#### 설정 변경 + +- General 옵션에서 Highlight updates when components render 옵션을 키면 리렌더링 되는 컴포넌트 하이라이트 + +## 6.4 정리 + +- 디버그 툴을 사용하면 시간에 따른 컴포넌트의 변화 상황 혹은, 불필요한 리렌더링 발생 여부 등을 체크 가능 +- 꾸준히 한다면 디버깅에 익숙해 질 수 있을 것 + +# 07. 크롬 개발자 도구를 활용한 애플리케이션 분석 + +## 7.1 크롬 개발자 도구란? + +- 크롬에서 제공하는 개발자용 도구, 웹에서 일어나는 거의 모든 일을 확인 할 수 있음 +- 실제로 어플리케이션 디버깅을 위해서는 시크릿 모드나, 프라이빗 모드로 여는 것을 권장 -> 크롬 확장 프로그램이 불필요한 정보를 추가하는 것을 막아준다 + +## 7.2 요소 탭 + +- 중단점 지정으로 디버깅에 편리함 제공 + +## 7.3 소스 탭 + +- 실제 파일의 소스 코드 확인 가능 +- 소스 코드에 중단점을 걸어 디버깅 가능 + +## 7.4 네트워크 탭 + +- 네트워크에서 발생하는 정보 확인 가능 + +## 7.5 메모리 탭 + +- 웹페이지가 차지하고 있는 메모리 관련 정보 확인 가능 +- 특정 객체를 우클릭 -> 전역 변수로 저장을 누르면 window.temp1 에 기록 되어 콘솔에서 확인이 가능하다 +- 스냅샷을 2개 찍어서 그 사이의 값 변경을 확인하는 방법이 편리하다 +- 해당 탭을 통하여 memo, useMemo, useCallback 의 의존성 값들이 실제로 그대로 유지되는 것 확인이 가능 + +## 7.6 Next.js 환경 디버깅하기 + +- SSR 의 메모리 누수는 사용자의 기기에서 발생하는 것이 아니라 서버 자체에서 발생하므로 서비스 자체에 부담 + 비용의 증가로 이어질 수 있으므로 잘 관리해야 한다 + +### 7.6.1 Next.js 프로젝트를 디버그 모드로 실행하기 + +- package.json 파일에 명령어를 수정 + +```json + "dev": "NODE_OPTIONS='inspect' next dev", +``` + +- chrome://inspect 로 이동하여, Open dedicated DevTools for Node 클릭하여 디버깅 모드 실행 + +### 7.6.2 Next.js 서버에 트래픽 유입시키기 + +- 아파치의 ab 명령어로 서버 성능 검사 (50개의 요청을 10000번 시도) + +```bash +ab -k -c 50 -n 10000 "http://127.0.0.1:3000/" +``` + +## 7.7 정리 + +- 개발자 도구를 사용하여 다양한 디버깅이 가능하다! diff --git a/pages/6week/0uizi0/study.mdx b/pages/6week/0uizi0/study.mdx index 1c8f9aa..37bf86f 100644 --- a/pages/6week/0uizi0/study.mdx +++ b/pages/6week/0uizi0/study.mdx @@ -1 +1,259 @@ -## 정리하기 파일 +## 8장. 좋은 리액트 코드 작성을 위한 환경 구축하기 + +### ESLint + +**ESLint 분석 과정** + +1. 자바스크립트 코드를 문자열로 읽는다 +2. 자바스크립트 코드를 분석할 수 있는 파서(parser)로 코드를 구조화한다 (→ 이때 espree를 사용한다.) +3. 2번에서 구조화한 트리를 AST(Abstract Syntax Tree)라 하며, 이 구조화된 트리를 기준으로 각종 규칙과 대조한다. +4. 규칙과 대조했을 때 이를 위반한 코드를 알리거나(report) 수정한다(fix). + +**코드 분석 도구 express** + +- ESLint는 JS를 분석하는 파서(parser)로, espree를 사용한다 +- 단순히 변수인지, 함수인지 함수명은 무엇인지 등만 파악하는 것이 아니라 코드의 정확한 위치와 같은 아주 세세한 정보도 분석해 알려준다 + +**ESLint 규칙으로, debugger 사용을 금지하는 규칙 생성하기** + +- ESLint 규칙(rules) : ESLint가 espree로 코드를 분석한 결과를 바탕으로, 어떤 코드가 잘못된 코드이며 어떻게 수정할 지 정하는 것 + +```jsx +// no-debugger 규칙 +module.exports = { + meta: { + type: "problem", + }, + docs: { + description: "Disallow the use of `debugger`", + recommended: true, + url: "https://eslint.org/docs/rules/no-debugger", + }, + fixable: null, + schema: [], + message: { + unexpected: "Unexpected 'debugger' statement.", + }, + create(context) { + return { + DebuggerStatement(node) { + context.report({ + node, + messageId: "unexpected", + }); + }, + }; + }, +}; +``` + +- meta : 해당 규칙과 관련된 메타 정보 +- messages : 규칙을 어겼을 때 반환하는 경고 문구 +- docs : 문서화에 필요한 정보 +- fixable : eslint --fix로 수정했을 때 수정 가능한지 여부 +- create : 코드에서 문제점을 확인하는 곳 + +**ESLint 관련 npm 패키지, `eslint-plugin`** + +- ESLint rules을 모아놓은 패키지 +- 리액트, import와 같이 특정 프레임워크나 도메인과 관련된 규칙을 묶어서 제공하는 패키지 +- 규칙 : 접두사를 준수해야 하며, 반드시 한 단어로 구성해야 한다 + +**ESLint 관련 npm 패키지, `eslint-config`** + +- eslint-plugin을 한데 묶어서 완벽하게 한 세트로 제공하는 패키지 +- 규칙 : 접두사를 준수해야 하며, 반드시 한 단어로 구성해야 한다 + +**ESLint-config 라이브러리, `eslint-config-airbnb`** + +- 가장 많이 적용되는 ESLint + +**ESLint-config 라이브러리, `@titicaca/triple-config-kit`** + +- 대부분의 eslint-config가 eslint-config-airbnb를 기반으로 약간의 룰을 수정해 배포되고 있는 것과 다르게 해당 패키지는 자체적으로 정의한 규칙을 기반으로 운영되고 있다 +- 외부로 제공하는 규칙에 대한 테스트코드가 존재한다 +- CI/CD 환경, 카나리 배포 등 일반적인 npm 라이브러리 구축 및 관리를 위한 시스템이 잘 구축돼 있다 +- 별도의 frontend 규칙도 제공하고 있어 Node.js 환경 또는 리액트 환경에 맞는 규칙을 적용할 수 있다는 장점이 있다. +- Prettier와 Stylelint를 각각 별도의 룰인 @titicaca/prettier-config-triple, @titicaca/stylelint-config-triple로 모노레포를 만들어 관리하고 있어 Prettier와 Stylelint도 필요에 따라 설치해서 사용할 수 있다 + +**ESLint-config 라이브러리, `eslint-config-next`** + +- 리액트 기반 Next.js 프레임워크를 사용하고 있는 프로젝트에서 사용할 수 있는 eslint-config +- 단순히 자바스크립트 코드를 정적으로 분석할 뿐만 아니라 페이지나 컴포넌트에서 반환하는 JSX 구문 및 \_app, \_document에서 작성돼 있는 HTML 코드 또한 정적 분석 대상으로 분류해 제공한다 +- 이는 단순히 자바스크립트 코드에 대한 향상뿐만 아니라 전체적인 Next.js 기반 웹 서비스의 성능 향상에 도움이 될 수 있다는 점에서 매우 유용하다 + +**import React 제거하기** + +- 동일한 두 코드에서, import React를 제거하는 것은 여전히 유용하다. +- 웹팩이 트리쉐이킹을 하는 데 걸리는 시간을 그만큼 줄일 수 있기 때문이다. +- 트리쉐이킹에 소요되는 시간이 없어진다면 자연스럽게 빌드 속도 또한 빨라질 것이다. +- 트리쉐이킹 : 코드 어디에서도 사용하지 않는 코드(dead code, 이른바 죽은 코드)를 삭제해서 최종 번들 크기를 줄이는 과정 + +**ESLint 사용 시 주의점** + +1. Prettier와의 충돌 + +- ESLint는 코드의 잠재적인 문제가 될 수 있는 부분을 분석해 준다면, Prettier는 포매팅과 관련된 작업, 즉 줄바꿈, 들여쓰기, 작은따옴표와 큰따옴표 등을 담당한다 +- 즉, ESLint에서도 Prettier에서 처리하는 작업을 처리할 수 있기 때문에 두 가지 모두를 자바스크립트 코드에서 실행한다면 서로 충돌하는 규칙으로 인해 에러가 발생한다. +- 해결 방법 1. 서로 규칙이 충돌되지 않게끔 규칙을 잘 선언하는 것 (ESLint에서 Prettier 규칙을 끈다.) +- 해결 방법 2. 자바스크립트나 타입스크립트는 ESLint에, 그 외의 파일(마크다운, YAML, JSON 등)은 모두 Prettier에 맡긴다. 그 대신 자바스크립트에 추가적으로 필요한 Prettier 관련 규칙은 모두 eslint-plugin-prettier를 사용한다. + +**규칙에 대한 예외 처리, 그리고 react-hooks/no-exhaustive-deps** + +- eslint-disable- 주석 : 일부 코드에서 특정 규칙을 임시로 제외시키고 싶을 때 사용하는 주석 +- eslint-disable-line no-exhaustive-deps : useEffect나 useMemo와 같이 의존 배열이 필요한 훅에 의존성 배열을 제대로 선언했는지 확인한다. + +**eslint-disable-line no-exhaustive-deps의 무지성 사용 시, 무엇이 잘못되었는가** + +- 임의로 판단해 괜찮다고 판단하여 사용하는 것은 대부분의 경우에 위험한 발상이며, 잠재적인 버그를 야기한다. +- **괜찮다고 임의로 판단한 경우** : 해당 변수는 컴포넌트의 상태와 별개로 동작한다는 것을 의미한다. 이 경우에는 해당 변수를 어디서 어떻게 선언할지 다시 고민해봐야 한다. +- **의존성 배열이 너무 긴 경우** : useEffect가 너무 길다는 것. useEffect를 쪼개서 의존성 배열의 가독성과 안정성을 확보해야한다. +- **마운트 시점에 한 번만 실행하고 싶은 경우** : [ ] 배열이 있다는 것은 컴포넌트 상태값과 별개의 부수 효과가 되어 컴포넌트의 상태와 불일치가 일어날 수 있게 된다. 마지막으로, 상태와 관계없이 한 번만 실행돼야 하는 것이 있다면 해당 컴포넌트에 존재할 이유가 없다. 이 경우 적절한 위치로 옮기는 것이 좋다. + +**ESLint의 버전 충돌** + +- ESLint의 버전 충돌 시, 에러가 발생한다. +- ESLint 공식 문서에는 ESLint를 peerDependencies로 설정해두라고 권장한다. +- 이러한 문제를 미연에 방지하려면 설치하고자 하는 eslint-config, eslint-plugin이 지원하는 ESLint 버전을 확인하고, 또 설치하고자 하는 프로젝트에서 ESLint 버전을 어떻게 지원하고 있는지 살펴봐야 한다. + +### 리액트 테스트 라이브러리 + +**테스트** + +- 개발자가 만든 프로그램이 코딩을 한 의도대로 작동하는지 확인하는 일련의 작업 +- 처음에 설계한 대로 프로그램이 작동하는지 확인 가능 +- 버그를 사전에 방지할 수 있음 +- 잘못된 작동으로 인해 발생하는 비용을 줄일 수 있음 +- 수정한 내용에 대해서도 예외 케이스가 없고 의도한 대로 작동할 수 있는지 확인 가능 +- 사용자에게 버그가 최소화된 안정적인 서비스를 제공 + +**프론트엔드의 테스트** + +- 백엔드의 테스트와 달리, 일반적인 사용자와 동일하거나 유사한 환경에서 수행된다. +- 주요 비즈니스 로직이나 모든 경우의 수를 고려해야 한다 +- 사용자에게 완전히 노출된 영역이므로 최대한 예측해서 확인해야 한다. 사용자는 개발자의 의도대로만 사용하지 않는다. + +**React Testing Library란?** + +- DOM Testing Library를 기반으로 만들어진 테스팅 라이브러리 +- DOM Testing Library : jsdom을 기반으로 함. → 자바스크립트 환경에서도 HTML을 사용할 수 있으므로 이를 기반으로 DOM Testing Library에서 제공하는 API를 사용해 테스트를 수행할 수 있다. +- 리액트 테스팅 라이브러리를 활용하면 실제로 리액트 컴포넌트를 렌더링하지 않고도, 즉 브라우저를 직접 실행해 눈으로 확인하지 않아도 리액트 컴포넌트가 원하는 대로 렌더링되고 있는지 확인할 수 있다. +- 이 방식은 테스트 환경을 구축하는 데 복잡한 과정을 거치지 않아 간편하고, 테스트에 소요되는 시간 역시 효과적으로 단축시킬 수 있다. +- 그리고 컴포넌트 뿐만 아니라 Provider, 훅 등 리액트를 구성하는 다양한 요소들을 테스트할 수 있다 + +**테스트 코드** + +- 내가 작성한 코드가 내가 코드를 작성했던 당시의 의도와 목적에 맞는지 확인하는 코드 +- Node.js는 `assert`라는 모듈을 기본적으로 제공한다. +- assert는 테스트 코드가 예상대로 작동한다고 ‘주장’하는 코드를 작성하면 이 코드의 성공 여부에 따라 테스트 통과 또는 실패를 반환한다. +- 테스트 코드와 실제 코드는 파일을 분리해 작성한다 +- 어설션(assertion) 라이브러리 : 테스트 결과를 확인할 수 있도록 도와주는 라이브러리. assert, should.js, expect.js, chai 등이 있다. + +**기본적인 테스트 코드를 작성하는 방식** + +1. 테스트할 함수나 모듈을 정한다 +2. 함수나 모듈이 반환하길 기대하는 값을 정한다 +3. 함수나 모듈의 실제 반환 값을 적는다 +4. 3번의 기대에 따라 2번의 결과가 일치하는지 확인한다 +5. 기대하는 결과를 반환한다면 테스트는 성공이며, 만약 기대와 다른 결과를 반환하면 에러를 던진다 + +**좋은 테스트 코드는,** + +- 가능한 한 사람이 읽기 쉽게, 그리고 테스트의 목적이 분명하게 작성되는 것이 중요하다 +- 다양한 테스트 코드가 작성되고 통과하는 것뿐만 아니라 어떤 테스트가 무엇을 테스트하는지 일목요연하게 보여주는 것도 중요하다. +- 이러한 테스트의 기승전결을 완성해주는 것이 `테스팅 프레임워크`이다 + +**테스팅 프레임워크** + +- 어설션을 기반으로 테스트를 수행하며, 여기에 추가로 테스트 코드 작성자에게 도움이 될 만한 정보를 알려주는 역할도 함께 수행한다 +- Jest, Mocha, Karma, Jasmine 등 + +**Jest 테스트 코드** + +- Node.js의 assert만 사용했을 때는 단순히 실패에 대해서만 단편적인 정보로 알 수 있었지만 Jest를 비롯한 테스트 프레임워크를 사용하면 무엇을 테스트했는지, 소요된 시간은 어느 정도인지, 무엇이 성공하고 실패했는지, 전체 결과는 어떤지에 대한 자세한 정보를 확인할 수 있다 +- test, expect 등의 메서드를 import나 require 같은 모듈을 불러오기 위해 사용하는 구문 없이 바로 사용했다 +- node가 아닌 jest(npm run test)로 실행했다. 만약 node로 실행했다면 Jest로 작성한 코드의 test, expect 모두 Node.js 환경의 global, 즉 전역 스코프에 존재하지 않는 메서드이기 때문에 에러가 발생했을 것이다. +- Jest를 비롯한 테스팅 프레임워크에는 이른바 글로벌 (global)이라 해서 실행 시에 전역 스코프에 기본적으로 넣어주는 값들이 있다. 그리고 Jest는 이 값을 실제 테스트 직전에 미리 전역 스코프에 넣어준다. 이렇게 하면 일일이 테스트에 관련한 정보를 임포트하지 않고도 사용할 수 있게 되는 것이다. 이는 간결하고 빠른 테스트 코드 작성에 도움을 준다. + +**리액트 컴포넌트 테스트** + +1. 컴포넌트를 렌더링한다 +2. 필요하다면 컴포넌트에서 특정 액션을 수행한다 +3. 컴포넌트 렌더링과 2번의 액션을 통해 기대하는 결과와 실제 결과를 비교한다 + +**리액트 컴포넌트 테스트에서 특정한 무언가를 지닌 HTML 요소 확인하기** + +- getBy… : 인수의 조건에 맞는 요소를 반환하며, 해당 요소가 없거나 두 개 이상이면 에러를 발생시킨다. +- findBy… : getBy…와 거의 유사하나 한 가지 큰 차이점은 Promise를 반환한다는 것이다. 즉, 비동기로 찾는다. +- queryBy… : 인수의 조건에 맞는 요소를 반환하는 대신, 찾지 못한다면 null을 반환한다. getBy…와 findBy…는 찾지 못하면 에러를 발생시키기 때문에 찾지 못해도 에러를 발생시키지 않고 싶을 때 사용한다. + +**OO 컴포넌트 테스트하기** + +1. 정적 컴포넌트 + +- 별도의 상태가 존재하지 않아 항상 같은 결과를 반환하는 컴포넌트 +- 테스트를 원하는 컴포넌트를 렌더링한 다음, 테스트를 원하는 요소를 찾아 원하는 테스트를 수행한다 + +> 이때 사용되는 `jest의 메서드` +> +> - beforeEach : 각 테스트(it)를 수행하기 전에 실행하는 함수 +> - describe : 비슷한 속성을 가진 테스트를 하나의 그룹으로 묶는 역할. 테스트코드가 많아지고 관리가 어려울 때 사용한다. +> - it : test의 축약어 +> - testId : 리액트 테스팅 라이브러리의 예약어로, get 등의 선택자로 선택하기 어렵거나 곤란한 요소를 선택하기 위해 사용할 수 있다. testId 데이터셋을 선언해두면 이후 테스트 시에 getByTestId, findByTestId 등으로 선택할 수 있다. +> - 데이터셋 : HTML의 특정 요소와 관련된 임의 정보를 추가할 수 있는 속성 + +- 각 테스트를 수행하기 전에 StaticComponent를 렌더링하고, describe로 연관된 테스트를 묶어서 it로 it 함수 내부에 정의된 테스트를 수행한다. + +2. 동적 컴포넌트 + +- 사용자가 useState를 통해 입력을 변경하는 컴포넌트 + +> 이때 사용되는 `jest의 메서드` +> +> - setup 함수 : 내부에서 컴포넌트를 렌더링하고, 또 테스트에 필요한 button과 input을 반환한다 +> - userEvent.type : 사용자가 타이핑하는 것을 흉내내는 메서드. 사용자의 작동을 여러 fireEvent를 통해 좀 더 자세하게 흉내 내는 모듈이다. 대부분의 이벤트를 테스트할 때는 fireEvent로 충분하고 훨씬 더 빠르기 때문에, 특별히 사용자의 이벤트를 흉내내야 할 때만 userEvent를 사용한다. +> - jest.spyOn(window, ‘alert’).mockImplementation() : 아래 두 메서드 참고 +> - jest.spyOn : 어떠한 특정 메서드를 오염시키지 않고 실행이 됐는지, 또 어떤 인수로 실행됐는지 등 실행과 관련된 정보만 얻고싶을 때 사용한다. 단순히 관찰하는 용도. +> - mockImplementation : 해당 메서드에 대한 모킹(mocking) 구현을 도와줌. 비록 모의 함수로 구현된 함수이지만 함수가 실행됐는지 등의 정보는 확인할 수 있도록 도와준다. + +3. 비동기 이벤트가 발생하는 컴포넌트 + +- MSW(Mock Service Worker) : Node.js나 브라우저에서 모두 사용할 수 있는 모킹 라이브러리 +- Node.js나 브라우저에서는 fetch 요청을 하는 것과 동일하게 네트워크 요청을 수행하고, 이 요청을 중간에 MSW가 감지하고 미리 준비한 모킹 데이터를 제공하는 방식이다. 이러한 방식은 fetch의 모든 기능을 그대로 사용하면서도 응답에 대해서만 모킹할 수 있으므로 fetch를 모킹하는 것이 훨씬 수월해진다. + +> 이때 사용되는 `MSW의 메서드` +> +> - setupServer : 서버를 만드는 역할. 해당 함수 내부에서 Express나 Koa와 비슷하게 라우트를 선언할 수 있다. + +- 테스트 코드를 시작하기 전에는 서버를 기동하고, 테스트 코드 실행이 종료되면 서버를 종료시킨다. + +```jsx +beforeAll(() => server.listen()); +afterEach(() => server.resetHandlers()); // 앞에서 선언한 setupServer의 기본 설정으로 되돌리는 역할. **서버 실패 테스트** 이후 서버를 초기화할 필요가 있기 때문. +afterAll(() => server.close()); +``` + +- 원하는 값을 동기 방식으로 즉시 찾는 get 메서드 대신, 요소가 렌더링될 때까지 일정 시간 동안 기다리는 find 메서드를 사용해 요소를 검색한다 +- 여기서 중요한 것은 MSW를 사용 한 fetch 응답 모킹과 findBy를 활용해 비동기 요청이 끝난 뒤에 제대로 된 렌더링이 일어났는지 기다린 후에 확인하는 것이다. 이 두 가지만 염두에 둔다면 비동기 컴포넌트 테스트 또한 크게 다를 것이 없다. + +4. 사용자 정의 훅 테스트하기 + +- react-hooks-testing-library를 사용해 기존 테스트코드 방식의 불편함을 해결한다 +- 매번 테스트가 끝난 후에는 process.env.NODE_ENV를 다시 development로 변경한다. NODE_ENV 할당문을 강제로 작성한 이유는 타입스크립트에서는 NODE_ENV를 읽기 전용 속성으로 간주하기 때문이다. +- 훅을 두 번 연속 실행하는 것을 테스트하기 위해서는 renderHook이 반환하는 객체의 값 중 하나인 rerender 함수를 사용해야 한다. + +**테스트 작성 시 고려해야 할 점** + +- 테스트 커버리지 : 해당 소프트웨어가 얼마나 테스트됐는지를 나타내는 지표 +- 테스트 커버리지가 높을수록 좋고 꾸준히 테스트 코드를 작성해야 한다. +- 그러나 테스트 커버리지는 단순히 얼마나 많은 코드가 테스트되고 있는지를 나타내는 지표일 뿐, 테스트가 잘되고 있는지를 나타내는 것은 아니다. 그러므로 절대 테스트 커버리지를 맹신해서는 안된다. +- 테스트 커버리지를 100% 끌어올릴 수 있는 상황은 생각보다 드물다. +- TDD(Test Driven Development; 테스트 주도 개발) 개발 방법론을 차용해서 테스트를 우선시하더라도 서버 코드와는 다르게 프론트엔드 코드는 사용자의 입력이 매우 자유롭기 때문에 이러한 모든 상황을 커버해 테스트를 작성하기란 불가능하다. +- 따라서 테스트 코드를 작성하기 전에 생각해봐야 할 최우선 과제는 **애플리케이션에서 가장 취약하거나 중요한 부분을 파악하는 것**이다. +- 테스트가 이뤄야할 목표는 **애플리케이션이 비즈니스 요구사항을 충족하는지 확인하는 것.** + +**테스트 방법들** + +- 유닛 테스트 : 각각의 코드나 컴포넌트가 독립적으로 분리된 환경에서 의도된 대로 정확히 작동하는지 검증하는 테스트 +- 통합 테스트 : 유닛 테스트를 통과한 여러 컴포넌트가 묶여서 하나의 기능으로 정상적으로 작동하는지 확인하는 테스트 +- 엔드 투 엔드: 실제 사용자처럼 작동하는 로봇을 활용해 애플리케이션의 전체적인 기능을 확인하는 테스트 diff --git a/pages/6week/jkyeun/about.mdx b/pages/6week/jkyeun/about.mdx index df3be89..da439ea 100644 --- a/pages/6week/jkyeun/about.mdx +++ b/pages/6week/jkyeun/about.mdx @@ -1 +1,3 @@ -## 이야기해보기 파일 +- ESLint가 이렇게까지 책으로 읽으면서 다뤄야 할 주제인가 ..? +- 테스트 코드는 항상 마음의 짐 +- 동진이형 그냥 밥 많이 먹고 행복해 diff --git a/pages/6week/jkyeun/study.mdx b/pages/6week/jkyeun/study.mdx index 1c8f9aa..ce5fb08 100644 --- a/pages/6week/jkyeun/study.mdx +++ b/pages/6week/jkyeun/study.mdx @@ -1 +1,100 @@ -## 정리하기 파일 +# [8장] 좋은 리액트 코드 작성을 위한 환경 구축하기 + +## ESLint를 활용한 정적 코드 분석 + +- 자바스크립트 생태계에서 가장 많이 사용되는 정적 코드 분석 도구는 바로 `ESLint`다 +- ESLint는 기본값으로 `espree`를 사용한다. +- `eslint-plugin`이라는 접두사로 시작하는 플러그인은 앞서 언급했던 규칙을 모아놓은 패키지다. +- `eslint-config`는 이러한 eslint-plugin을 한데 묶어서 완벽하게 한 세트로 제공하는 패키지라 할 수 있다. +- new Date()는 기기에 의존적이다. 이를 해결하려면 서버의 시간에 의존하도록 해야한다. +- Prettier와 ESLint가 충돌할 때 해결할 수 있는 방법 중 하나는 자바스크립트나 타입스크립트는 ESLint에, 그 외의 파일은 모두 Prettier에게 맡기는 것이다. + +### 규칙에 대한 예외 처리 + +- 리액트 개발자라면 이러한 규칙을 가장 많이 사용하는 곳 중 하나가 바로 // eslint-disable-line no-exhaustive-deps일 것이다. +- 그러나 일반적으로 리액트 개발자들은 개발 시 이 의존성 배열이 너무 길어지거나, 혹은 빈 배열을 넣어서 컴포넌트가 마운트되는 시점에 한 번만 강제로 실행되게 하고 싶을 때, 혹은 임의로 판단해 없어도 괜찮다고 생각될 때 등에 사용한다. +- 그러나 이것은 대부분의 경우에 위험한 발상이며, 잠재적인 버그를 야기할 수 있다. + +### 정리 + +- ESLint를 잘 쓰고 있는 개발자라고 하더라도 꼭 프로젝트에 설치돼 있는 eslint-config는 무엇인지, 왜 이것을 문제가 있는 코드로 간주하는지 반드시 한 번씩 살펴보길 바란다. + +## 리액트 팀이 권장하는 리액트 테스트 라이브러리 + +- 테스트를 통해 개발자들은 처음에 설계한 대로 프로그램이 작동하는지 확인할 수 있고, 버그를 사전에 방지할 수도 있으며, 이후에 잘못된 작동으로 인해 발생하는 비용을 줄일 수도 있다. +- React Testing Library(이하 리액트 테스팅 라이브러리)란 DOM Testing Library를 기반으로 만들어진 테스팅 라이브러리로, 리액트를 기반으로 한 테스트를 수행하기 위해 만들어졌다. +- jsdom을 사용하면 마치 HTML이 있는 것처럼 DOM을 불러오고 조작할 수 있다. 리액트 테스팅 라이브러리는 jsdom을 기반으로 하는 DOM Testing Library이다. +- Node.js는 assert?26??라는 모듈을 기본적으로 제공하며, 이 모듈을 사용하면 위와 같이 작동하도록 만들 수 있다. + +### RTL의 개념들 + +#### beforeEach + +- 각 테스트(it)를 수행하기 전에 실행하는 함수다. +- 여기서는 각 테스트를 실행하기에 앞서 Static Component를 렌더링한다. + +#### describe + +- 비슷한 속성을 가진 테스트를 하나의 그룹으로 묶는 역할을 한다. +- 정의에서도 알 수 있듯, 이 describe는 꼭 필요한 메서드는 아니다. 그러나 테스트 코드가 많아지고 관리가 어려워진다면 describe로 묶어서 관리하는 것이 편리하다. +- describe 내부에 describe를 또 사용할 수 있다. + +#### it + +- test와 완전히 동일하며, test의 축약어(alias)다. +- it이라는 축약어를 제공하는 이유는 테스트 코드를 좀 더 사람이 읽기 쉽게 하기 위해서다 +- describe ... it (something)과 같은 형태로 작성해 두면 테스트 코드가 한결 더 문어체 같이 표현되어 읽기 쉬워진다. + +#### testId + +- testId는 리액트 테스팅 라이브러리의 예약어로, get 등의 선택자로 선택하기 어렵거나 곤란한 요소를 선택하기 위해 사용할 수 있다. +- HTML의 DOM 요소에 testId 데이터셋을 선언해 두면 이후 테스트 시에 getByTestId, findByTestId 등으로 선택할 수 있다. +- 웹에서 사용하는 `querySelector([data-testid="${yourId}"])`와 동일한 역할을 한다. + +- dataset이 무엇인가요? + - 데이터셋이란 HTML의 특정 요소와 관련된 임의 정보를 추가할 수 있는 HTML 속성이다. + - HTML의 특정 요소에 data-로 시작하는 속성은 무엇이든 사용할 수 있다. 앞의 예제에서는 HTML에 data-testid를 추가해 getByTestId를 사용했다. + - 이는 특정 시나리오에서 매우 유용하게 사용할 수 있다. + +#### userEvent.type + +- userEvent.type은 사용자가 타이핑하는 것을 흉내 내는 메서드다. +- userEvent.type을 사용하면 사용자가 키보드로 타이핑하는 것과 동일한 작동을 만들 수 있다. +- userEvent는 @testing-library/react에서 제공하는 fireEvent와 차이가 있다. 기본적으로 userEvent는 fireEvent의 여러 이벤트를 순차적으로 실행해 좀 더 자세하게 사용자의 작동을 흉내 낸다. + +#### jest.spyOn + +- Jest가 제공하는 spyOn은 어떠한 특정 메서드를 오염시키지 않고 실행이 됐는지, 또 어떤 인수로 실행됐는지 등 실행과 관련된 정보만 얻고 싶을 때 사용한다. +- jest.spyOn으로 calc 객체의 add 메서드를 관찰하는 것을 볼 수 있다. spyOn으로 관찰한 덕분에 한 번 호출됐는지(toBeCalledTimes(1)), 원하는 인수와 함께 호출됐는지(toBeCalledWith(1, 2))를 확인할 수 있다. +- 그리고 spyOn으로 관찰은 했지만 calc.add의 작동 자체에는 영향을 미치지 않은 것을 확인할 수 있다. + +#### mockImplementation + +- 해당 메서드에 대한 모킹 구현을 도와준다. + +#### MSW + +- MSW는 Node.js나 브라우저에서 모두 사용할 수 있는 모킹 라이브러리로, 브라우저에서는 서비스 워커를 활용해 실제 네트워크 요청을 가로채는 방식으로 모킹을 구현한다. +- 그리고 Node.js 환경에서는 https나 XMLHttpRequest의 요청을 가로채는 방식으로 작동한다. +- 즉, Node.js나 브라우저에서는 fetch 요청을 하는 것과 동일하게 네트워크 요청을 수행하고, 이 요청을 중간에 MSW가 감지하고 미리 준비한 모킹 데이터를 제공하는 방식이다. + +### 사용자 정의 훅 테스트하기 + +- `react-hooks-testing-library`를 활용하면 훅을 더욱 편리하게 테스트할 수 있다. +- 프로젝트가 리액트 18 버전 미만을 사용한다면 @testing-library/react 대신 @testing-library/react-hooks를 사용해야 한다. 리액트 18부터는 @testing-library/react에 통합됐다. +- 같은 컴포넌트에서 훅을 두 번 호출하려면 renderHook이 반환하는 객체의 값 중 하나인 rerender 함수를 사용해야 한다. +- rerender 외에도 unmount라는 함수를 반환하는데, 이름 그대로 이 함수를 실행하면 컴포넌트를 언마운트한다. + +### 정리 + +- 테스트 커버리지는 단순히 얼마나 많은 코드가 테스트되고 있는지를 나타내는 지표일 뿐, 테스트가 잘되고 있는지를 나타내는 것은 아니다. 그러므로 절대 테스트 커버리지를 맹신해서는 안 된다. +- TDD(Test Driven Development; 테스트 주도 개발)라고 하는 개발 방법론을 차용해서 테스트를 우선시하더라도 서버 코드와는 다르게 프런트엔드 코드는 사용자의 입력이 매우 자유롭기 때문에 이러한 모든 상황을 커버해 테스트를 작성하기란 불가능하다. +- 실무에서는 테스트 코드를 작성하고 운영할 만큼 여유로운 상황이 별로 없다. 때로는 테스트를 QA(Quality Assurance)에 의존해 개발을 빠르게 진행해야 할 수도 있고, 이후에 또 개발해야 할 기능이 산적해 있을 수도 있다. +- 테스트 코드를 작성하기 전에 생각해 봐야 할 최우선 과제는 애플리케이션에서 가장 취약하거나 중요한 부분을 파악하는 것이다. +- 테스트 코드는 개발자가 단순 코드 작성만으로는 쉽게 이룰 수 없는 목표인 소프트웨어 품질에 대한 확신을 얻기 위해 작성하는 것이다. +- 프런트엔드는 무작위 사용자가 애플리케이션에서 갖가지 작업을 할 수 있으므로 이를 테스트하기 위한 여러 가지 방법이 있다. + - 유닛 테스트(Unit Test): 각각의 코드나 컴포넌트가 독립적으로 분리된 환경에서 의도된 대로 정확히 작동하는지 검증하는 테스트 + - 통합 테스트(Integration Test): 유닛 테스트를 통과한 여러 컴포넌트가 묶여서 하나의 기능으로 정상적으로 작동하는지 확인하는 테스트 + - 엔드 투 엔드(End to End Test): 흔히 E2E 테스트라 하며, 실제 사용자처럼 작동하는 로봇을 활용해 애플리케이션의 전체적인 기능을 확인하는 테스트 +- 리액트 테스팅 라이브러리는 유닛 테스트 내지는 통합 테스트를 도와주는 도구이며, E2E 테스트를 수행하려면 `Cypress` 같은 다른 라이브러리의 힘을 빌려야 한다. +- 테스트할 수 있는 방법은 여러 가지가 있지만 테스트가 이뤄야 할 목표는 애플리케이션이 비즈니스 요구사항을 충족하는지 확인하는 것 한 가지뿐이다. diff --git a/pages/6week/kimjong22/study.mdx b/pages/6week/kimjong22/study.mdx index 1c8f9aa..3fb92e6 100644 --- a/pages/6week/kimjong22/study.mdx +++ b/pages/6week/kimjong22/study.mdx @@ -1 +1,199 @@ -## 정리하기 파일 +#### ESLint를 활용한 정적 코드 분석 + +버그와 예기치못한 작동을 방지하기 위한 방법으로 가장 빠르게 시도해 볼 수 있는 방법은 정적 코드 분석이다. 이는 코드 자체만으로 코드 스멜을 찾아내 사전에 수정하는 것을 의미한다. + +--- + +**ESLint는 어떻게 코드를 분석할까?** + +1. js 코드를 문자열로 읽는다. +2. js 코드를 분석할 수 있는 파서인 espree로 코드를 구조화한다. +3. 2번에서 구조화한 트리를 AST라고 하는데, 이를 기준으로 각종 규칙과 대조핳ㄴ다. +4. 규칙과 대조해 위반한 코드를 알리거나 수정한다. + +> [AST explorer](https://astexplorer.net/) +> 해당 사이트에서 espree나 다른 파서로 자바스크립트/타입스크립트 코드를 분석할 수 있다. + +```javascript +function astExploer() { + console.log('Hello World') +} + +// AST explorer에서 파서를 espree로 설정하고 분석한 결과 +{ + "type": "Program", + "start": 179, + "end": 229, + "range": [ + 179, + 229 + ], + "body": [ + { + "type": "FunctionDeclaration", + "start": 179, + "end": 229, + "range": [ + 179, + 229 + ], + "id": { + "type": "Identifier", + "start": 188, + "end": 198, + "range": [ + 188, + 198 + ], + "name": "astExploer" + }, + "expression": false, + "generator": false, + "async": false, + "params": [], + "body": { + "type": "BlockStatement", + "start": 201, + "end": 229, + "range": [ + 201, + 229 + ], + "body": [ + { + // ✅ 해당 코드의 표현식 전체를 나타낸다. + "type": "ExpressionStatement", + "start": 205, + "end": 227, + "range": [ + 205, + 227 + ], + // ✅ ExpressionStatement에 어떤 표현이 들어가 있는지 확인한다. + "expression": { + // 해당 표현이 어떤 타입인지 나타낸다. + "type": "CallExpression", + "start": 205, + "end": 227, + "range": [ + 205, + 227 + ], + // ✅ 생성자를 사용한 표현식에서 생성자의 이름을 나타낸다. + "callee": { + "type": "MemberExpression", + "start": 205, + "end": 216, + "range": [ + 205, + 216 + ], + "object": { + "type": "Identifier", + "start": 205, + "end": 212, + "range": [ + 205, + 212 + ], + "name": "console" + }, + "property": { + "type": "Identifier", + "start": 213, + "end": 216, + "range": [ + 213, + 216 + ], + "name": "log" + }, + "computed": false, + "optional": false + }, + // ✅ 생성자를 표현한 표현식에서 생성자에 전달하는 인수를 나타낸다. + "arguments": [ + { + "type": "Literal", + "start": 217, + "end": 226, + "range": [ + 217, + 226 + ], + "value": "Hello World", + "raw": "'Hello World'" + } + ], + "optional": false + } + } + ] + } + } + ], + "sourceType": "module" +} +``` + +#### eslint-plugin과 eslint-config + +eslint-plugin은 언급했던 규칙을 모아놓은 패키지이다. 특정 프레임워크나 도메인과 관련된 규칙을 묶어서 제공하는 패키지이다. eslint-config는 이러한 eslint-plugin을 한데 묶어 한 세트로 제공하는 패키지이다. + +--- + +**널리 사용되는 exlint-config의 종류** + +- eslint-config-airbnb +- eslint-config-next + - 페이지나 컴포넌트에서 반환히는 ]SX 구문 및 `_app`, `_document`에서 작성돼 있는 HTML 코드 또한 정적 분석 대상으로 분류해 제공 + - core web vitals 요소들을 분석하는 기능도 포함 + +> console.log를 쓰면 console.error를 쓰라고 create 필드로 fix 해주는 ESLint 규칙을 만들 수도 있다. + +```javascript +module.exports = { + create: function (context) { + return { + CallExpression(node) { + if ( + node.callee.object.name === "console" && + node.callee.property.name === "log" + ) { + context.report({ + node, + message: + "console.log를 사용하지 마세요. 대신 console.error를 사용하세요.", + fix: function (fixer) { + return fixer.replaceText(node.callee.property, "error"); + }, + }); + } + }, + }; + }, +}; +``` + +규칙은 하나씩 만들어서 배포하는 것은 배포하는 것이 불가능하며, 묶음으로 eslint-plugin 형태로 배포하는 것만 가능하다. + +#### 리액트 테스트 라이브러리란? + +DOM testing Library를 기반으로 만들어진 테스팅 라이브러리이다. DOM testing Library는 jodom을 기반으로 하고 있다. jsdom이란 순수하게 자바스크립트로 작성된 라이브러리이로, Node.js같은 환경에서도 HTML이나 DOM을 사용할 수 있도록 해주는 라이브러리다. jsdom을 사용하면 자바스크립트 환경에서도 마치 HTML을이 있는 것처럼 DOM을 불러오고 조작할 수 있다. + +--- + +- 유닛 테스트: 각각의 코드나 컴포넌트가 독립적으로 분리된 환경에서 의도된 대로 정확히 작동하는지 검증하는 테스트 +- 통합 테스트: 유닛 테스트를 통과한 컴포넌트가 묶여서 하나의 기능으로 정상적으로 작동하는지 확인하는 테스트 +- 엔드 투 엔드: E2E 테스트는 실제 사용자처럼 작동하는 로봇을 활용해 전체적인 기능을 확인하는 테스트 + +리액트 컴포넌트에서 테스트하는 일반적인 시나리오는 특정한 HTML 요소가 있는지의 여부를 확인한다. + +- getBy...: 인수의 조건에 맞는 요소를 반환한다. 복수는 getAllBy를 사용 +- findBy...: getBy와 유사하지만 Promise를 반환한다. 비동기로 찾으며 기본적으로 1000ms의 타임아웃으로 설정되어 있다. 복수는 findAllBy를 사용 +- queryBy...: 인수의 조건에 맞는 요소를 반환하는 대신 찾지 못한다면 에러를 발생시키지 않고 null을 반환한다. 복수개를 찾았을 때는 에러를 발생시키고, 복수는 queryAllBy를 사용 + +- beforeEach: 각 테스트(it)를 수행하기 전에 실행하는 함수이다. +- userEvent.type: 사용자가 타이핑 하는 것을 흉내내는 메서드이다. userEvent는 fireEvent의 여러 이벤트를 순차적으로 실행해 좀 더 자세하게 사용자의 작동을 흉내낸다. +- jest.spyOn: 특정 메서드를 오염시키지 않고 실행이 됐는지, 어떤 인수로 실행 됐는지 등 실행과 관련된 정보를 얻고 싶을 때 사용한다. `jest.spyOn(window, 'alert')` +- mockImplementation: 해당 메서드에 대한 모킹 구현을 도와준다. Jest를 실행하는 Node.js 환경에서는 window.alert가 존재하지 않으므로 해당 메서드를 모의 함수로 구현하는데, 해당 역할을 한다. diff --git a/pages/6week/xenosign/about.mdx b/pages/6week/xenosign/about.mdx index df3be89..e05fb0b 100644 --- a/pages/6week/xenosign/about.mdx +++ b/pages/6week/xenosign/about.mdx @@ -1 +1,168 @@ -## 이야기해보기 파일 +# JEST 관련 에러 + +- 단, React 18 버전으로 넘어오면서 약간의 warning 이 생기는 문제 발생 중 + +- 책에 나온 코드 + +```js +it("영문과 숫자만 입력된다.", () => { + const { input } = setup(); + const inputValue = "안녕하세요123"; + + // 사용자의 입력을 흉내내는 메서드, 사용자가 키보드로 타이핑을 하는 것을 테스트 가능 + userEvent.type(input, inputValue); + + expect(input.value).toEqual("123"); +}); +``` + +- userEvent 사용시 발생하는 Warning 상황 + +![warning 상황](./act_warning.png) + +- React 18 이상 버전에서 실제 문제 없이 작동하는 코드 + +```js +it("영문과 숫자만 입력된다.", () => { + const { input } = setup(); + const inputValue = "안녕하세요123"; + + // fireEvent 로 처리해야 문제가 생기지 않음 + fireEvent.change(input, { target: { value: inputValue } }); + + expect(input.value).toEqual("123"); +}); +``` + +- fireEvent 로 변경 후, 정상 작동하는 상황 + +![정상 상황](./no_warning.png) + +- 아마도, Testing Library 의 경우 테스트 이외의 상황에서 컴포넌트의 변화가 발생하면 테스트 결과가 맞아도 위와 같은 Warining 을 발생하는 것을 확인 가능 +- userEvent 의 경우 fireEvent 의 input 이 여러번 발생하는 구조이므로, setText 가 계속 적으로 컴포넌트를 업데이트 하는 상황이 발생하여 생기는 에러로 추측 +- 따라서 userEvent.type 이 아닌 fireEvent.change 로 한번에 값의 변화를 주고 테스트 하면 정상 테스트가 되는 것을 확인 가능 + + - 아이디어 참고 : https://kentcdodds.com/blog/fix-the-not-wrapped-in-act-warning + - 한번 쯤 보면 매우 좋을듯, 컴포넌트의 모든 상태 변화에 대한 이해를 바탕으로 테스트를 작성해야 한다는 점 + - 실제 사례를 보면, 비동기로 상태를 반영하기 때문에 promise 를 테스트 상황에서 예외 처리하는 것을 볼 수 있다 + + - 실제 해결 사례 내용 : https://github.com/testing-library/react-testing-library/issues/1051 + - React 18 버전으로 업데이트 되면서 생긴 문제라는 것으로 보아 리액트의 변화 사항을 testing-library 가 반영하지 못한 상태로 추측 + +- 문제 해결을 위해 참고한 블로그들 + + - https://flyingsquirrel.medium.com/testing-library-react%EC%9D%98-act%EB%8A%94-%EC%96%B8%EC%A0%9C-%EC%8D%A8%EC%95%BC%ED%95%A0%EA%B9%8C-c6036a8cd4b3 + + - https://kentcdodds.com/blog/common-mistakes-with-react-testing-library + - https://seongry.github.io/2021/06-20-common-mistakes-with-rty/ + - https://kentcdodds.com/blog/fix-the-not-wrapped-in-act-warning + - https://github.com/kentcdodds/fix-act-warning-demos/blob/main/src/__tests__/username-form.js + +# 비동기 컴포넌트에서 msw 이슈를 해결하기 위해 이리저리 삽질한 Story + +- CRA + MSW 2.0 버전에서의 이슈로 인하여, 2.0 문법을 사용하면 특정 모듈이 import 가 안되는 이슈가 발생 +- 해결을 위해 다양한 접근을 했지만, 모두다 제대로 동작하지 않는 이슈 발생 +- CRA 를 사용하지 않기 위해, Vite 를 사용하여 번들링 +- Vite 에, jest, msw 등등을 설치하고 테스트 +- Vite 에서 jest 사용을 위한 설정 참고 + - https://xionwcfm.tistory.com/369 +- 그래서 실행을 했으나, 아래와 같은 이슈 발생 + +![alt text](./vite-jest-1.png) + +- 이를 해결하기 위해 msw 공식 문서를 확인 + +![alt text](./vite-jest-2.png) + +- node v20 사용중 & jest.polifills.js 적용, 그러나 + +![alt text](./vite-jest-3.png) + +- 그래서 찾아보니 CRA 이슈로 node 환경에서 global 이 제대로 안불러와지는 문제라고 한다. 그래서 더 찾아보니 + +![두둥](./msw_migration.png) + +## 결론, vitest 로 갈아탈 타이밍이다 + +# Vite & Vitest 로 해당 코드를 돌리기 위해 삽질한 결과물 + +- vite 로 프로젝트 생성 + +```bash +# npm 6 이하 버전 일 경우 +npm create vite@latest [프로젝트 명] --template react-ts +# npm 7 이상 버전일 경우 +npm create vite@latest [프로젝트 명] -- --template react-ts + +# 직접 선택을 원할 경우, 아래의 명령어로 만들고 프로젝트명, 언어 및 프레임워크 선택 가능 +npm create vite@latest +``` + +- 필요 모듈 설치 (msw, vitest, jsdom, @testing-library/react, @testing-library/jest-dom) + +```bash +npm i -D msw vitest jsdom @testing-library/react @testing-library/jest-dom +``` + +- test 스크립트 추가 + +```json + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", + "preview": "vite preview", + // 추가 + "test": "vitest" + }, +``` + +- vite.confing.ts 에 테스트를 위한 세팅 필요 + +```ts +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; + +export default defineConfig({ + plugins: [react()], + // test 파트 추가 필요 + test: { + globals: true, + environment: "jsdom", + }, +}); +``` + +- msw 2.0 버전으로 코드 마이그레이션 (https://mswjs.io/docs/migrations/1.x-to-2.x) + +- 기존 jest 에서 사용하던 명령어를 vitest 라이브러리 것으로 교체 + +```tsx +import { + afterAll, + afterEach, + beforeAll, + beforeEach, + describe, + expect, + it, +} from "vitest"; +``` + +- 하지만, 아직 코드 내부에서 사용하는 몇몇 명령어는 이전의 라이브러리 것 사용 필요 +- 위의 라이브러리를 사용하지 않으면, 몇몇 에러 발생 (expect().toBeInTheDocument() 같은 명령어 사용 불가) + +```tsx +import { fireEvent, render, screen } from "@testing-library/react"; +import "@testing-library/jest-dom"; +``` + +- 테스트 수행 + +```bash +npm run test +``` + +![성공](./vite-success.png) + +## [p. 526] TDD 에 대한 의견은 어떠신가요? diff --git a/pages/6week/xenosign/act_warning.png b/pages/6week/xenosign/act_warning.png new file mode 100644 index 0000000..ed811bc Binary files /dev/null and b/pages/6week/xenosign/act_warning.png differ diff --git a/pages/6week/xenosign/msw_migration.png b/pages/6week/xenosign/msw_migration.png new file mode 100644 index 0000000..e541f91 Binary files /dev/null and b/pages/6week/xenosign/msw_migration.png differ diff --git a/pages/6week/xenosign/no_warning.png b/pages/6week/xenosign/no_warning.png new file mode 100644 index 0000000..58335c2 Binary files /dev/null and b/pages/6week/xenosign/no_warning.png differ diff --git a/pages/6week/xenosign/study.mdx b/pages/6week/xenosign/study.mdx index 1c8f9aa..dd96c68 100644 --- a/pages/6week/xenosign/study.mdx +++ b/pages/6week/xenosign/study.mdx @@ -1 +1,869 @@ -## 정리하기 파일 +# 08. 좋은 리액트 코드 작성을 위한 환경 구축하기 + +## 8.1 ESLint 를 활용한 정적 코드 분석 + +### 8.1.1 ESLint 살펴보기 + +#### ESLint 는 어떻게 코드를 분석할까? + +1. JS 를 문자열로 읽는다 +2. JS 를 분석할 수 있는 Parser 인 espee 를 사용하여 코드를 구조화 한다 +3. 2번에서 구조화한 트리(Abstract Syntax Tree)를 기준으로 각종 규칙을 대조 +4. 규칙과 대조했을 때 이를 위반한 코드를 Report or Fix + +### 8.1.2 eslint-plugin 과 eslint-config + +#### eslint-plugin + +- 다양한 ESLint 규칙을 모아놓은 패키지 + +#### eslint-config + +- ESLint 관련 설정을 제공하는 패키지 + +##### eslint-config-airbnb + +- Airbnb 에서 만들었으며, 500여 명의 개발자가 유지보수하는 가장 유명한 규칙 + +##### @@titicaca/triple-config-kit + +- 트리플에서 유지 관리하는 패키지 +- Airbnb 의 룰을 따르지 않고, 기본적인 규칙을 제공 + +##### eslint-config-next + +- Next.js 를 위한 듀칙 제공 + +### 8.1.3 나만의 ESLint 규칙 만들기 + +#### Import React 를 제거하기 위한 ESLint 규칙 만들기 + +```js +module.exports = { + rules: { + "no-restrict-imports": [ + "error", + { + paths: [ + { + name: "react", + importNames: ["default"], + message: "Import React from 'react'는 17버전 부터 필요 없습니다!", + }, + ], + }, + ], + }, +}; +``` + +#### 완전히 새로운 규칙 만들기: new Date 를 금지시키는 규칙 + +```js +/** + * @type {import('eslint').Rule.RuleModule} + */ + +module.exports = { + meta: { + type: "suggestion", + docs: { description: "disallow use of the new Date()", recommended: false }, + fixable: "code", + schema: [], + messages: { + message: + "new Date()는 클라이언트에서 실행 시 해당 기기의 시간에 의존적이라 정확하지 않습니다. 현재 시간이 필요하다면 ServerDate()를 사용해 주세요.", + }, + }, + create: function (context) { + return { + NewExpression: function (node) { + if (node.callee.name === "Date" && node.arguments.length === 0) { + context.report({ + node: node, + messageId: "message", + fix: function (fixer) { + return fixer.replaceText(node, "ServerDate()"); + }, + }); + } + }, + }; + }, +}; +``` + +### 8.1.4 주의할 점 + +#### Prettier 와의 충돌 + +#### 규칙에 대한 예외 처리, 그리고 React-hooks/no-exhaustive-deps + +- 일부 코드에서 특정 규칙을 임시로 제외시키고 싶다면, eslint-disable-주석 사용 필요 + +```js +// 특정 줄만 제외 +console.log("hello world"); // eslint-disable-line no-console + +// 다음 줄 제외 +// eslint-disable-next-line no-console +console.log("hello world"); + +// 특정 여러 줄 제외 +/* eslint-disable no-console */ +console.log("JavaScript debug log"); +console.log("eslint is disabled now"); +/* eslint-enable no-console */ + +// 파일 전체에서 제외 +/* eslint-disable no-console */ +console.log("hello world"); +``` + +- useEffect 나 useMemo 의 의존성 배열을 예외 처리하는 eslint-disable-line no-exhaustive-deps 를 자주 사용할 가능성이 높은데 잠재적 버그 유발이 가능하므로 주의가 필요하다. +- 그 외에 type-script 에서 any 를 강제로 사용하기 위한 typescript-eslint/no-explicit-any 도 마찬가지다 + +#### ESLint 의 버전 충돌 + +- 서로 다른 버전의 ESLint 가 설치 되어 있으면, 에러가 발생하므로 ESLint 의 의존성을 배포 의존성(peerDependecies)로 설정해 두는 것을 권장 + +### 8.1.5 정리 + +- 버그 예방, 공통된 코드 스타일 등등 사용을 권장 +- 사용은 안하더라도 사용법은 알아 둘 것! + +## 8.2 리액트 팀이 권장하는 리액트 테스트 라이브러리 + +### 8.2.1 React Testing Library 란? + +- DOM Testing Library 를 기반으로 만들어진 테스팅 라이브러리. +- jsdom 을 사용하여 nodes.js 환경에서 HTML 과 DOM 을 사용할 수 있게 하여 테스팅을 수행한다 +- 실제로 브라우저를 구동하여 컴포넌트를 렌더링하지 않고도, 해당 컴포넌트가 제대로 렌더링 되었는지를 확인하는 방식으로 테스팅 수행 + +### 8.2.2 JS 테스트의 기초 + +1. 테스트할 함수나 모듈 선정 +2. 함수나 모듈이 반환하기 기대하는 값 설정 +3. 함수나 모듈의 실제 반환값을 계산 +4. 3번과 2번을 비교 +5. 비교 결과가 같지 않을 경우 에러 발생 + +- 테스트 결과를 확인할 수 있도록 도와주는 라이브러리 = 어설션(Assertion) 라이브러리 +- 다만, 테스트를 위해서는 복잡한 일련의 과정이 필요하고 이를 지원하는 것이 테스팅 프레임워크. 대표저긍로 Jest, Mocha, Karma, Jasmine 등이 존재한다 + +#### Jest + +- 예시 코드 + +```js +// math.js +function sum(a, b) { + return a + b; +} + +module.exports = { + sum, +}; + +// math.test.js + +const { sum } = require("./math"); + +test("두 인수가 덧셈이 되어야 한다.", () => { + expect(sum(1, 2)).toBe(3); +}); + +// 에러 발생 테스트 +test("두 인수가 덧셈이 되어야 한다.", () => { + expect(sum(2, 2)).toBe(3); +}); +``` + +- Node.js 의 Assertion 을 쓴 것과는 다르게 테스트를 통해 걸린 시간, 무엇이 성공하고 실패하였는지 등등 자세한 정보를 제공 + +### 8.2.3 리액트 컴포넌트 테스트 코드 작성하기 + +#### HTML 요소를 확인하는 방법 + +- getBy : 인수의 조건에 맞는 요소를 반환, 요소가 없거나 2개 이상이면 에러 발생 +- getAllBy : 인수의 조건에 맞는 요소를 반환, 요소가 복수개여도 문제 없음 +- findBy : getBy 와 같은 역할을 하지만, Promise 를 반환. 기본적으로 1000ms 의 시간을 가지며 비동기 액션 이후의 요소를 검사할 때 사용한다. getBy 와 마찬가지로 복수의 요소를 검사할 땐 findAllBy 를 사용 +- queryBy : 인수의 조건에 맞는 요소를 반환하지만, 못찾으면 에러가 아닌 null 을 반환. 에러를 발생시키고 싶지 않은 경우 사용한다. 역시 복수의 요소 검사는 queryAllBy 를 사용 + +#### 정적 컴포넌트 + +- 별도의 상태가 존재하지 않으면 같은 결과를 반환하는 정적 컴포넌트의 테스트 방법은 쉽다 + +- 정적 컴포넌트 코드 + +```tsx +import { memo } from "react"; + +const AnchorTagComponent = memo(function AnchorTagComponent({ + name, + href, + targetBlank, +}: { + name: string; + href: string; + targetBlank?: boolean; +}) { + return ( +
+ {name} + + ); +}); + +export default function StaticComponent() { + return ( + <> +

Static Component

유용한 링크
+
    +
  • + +
  • +
  • + +
  • +
  • + +
  • +
+ + ); +} +``` + +- 테스트 코드 + +````tsx +import { render, screen } from "@testing-library/react"; +import StaticComponent from "./StaticComponent"; + +beforeEach(() => { + // eslint-disable-next-line + render(); +}); + +describe("링크 확인", () => { + it("링크가 3개 존재한다.", () => { + const ul = screen.getByTestId("ul"); + // eslint-disable-next-line + expect(ul.children.length).toBe(3); + }); + + it("링크 목록의 스타일이 square 다.", () => { + const ul = screen.getByTestId("ul"); + expect(ul).toHaveStyle("list-style-type: square"); + }); +}); + +describe("리액트 링크 테스트", () => { + it("리액트 링크가 존재한다", () => { + const reactLink = screen.getByText("리액트"); + expect(reactLink).toBeVisible(); + }); + + it("리액트 링크가 올바른 주소로 존재한다", () => { + const reactLink = screen.getByText("리액트"); + expect(reactLink.tagName).toEqual("A"); + expect(reactLink).toHaveAttribute("href", "https://reactjs.org"); + }); +}); + +describe("네이버 링크 테스트", () => { + it("네이버 링크가 존재한다", () => { + const naverLink = screen.getByText("네이버"); + expect(naverLink).toBeVisible(); + }); + + it("네이버 링크가 올바른 주소로 존재한다", () => { + const naverLink = screen.getByText("네이버"); + expect(naverLink.tagName).toEqual("A"); + expect(naverLink).toHaveAttribute("href", "https://www.naver.com"); + }); +}); + +describe("블로그 링크 테스트", () => { + it("블로그 링크가 존재한다", () => { + const blogLink = screen.getByText("블로그"); + expect(blogLink).toBeVisible(); + }); + + it("블로그 링크가 올바른 주소로 존재한다", () => { + const blogLink = screen.getByText("블로그"); + expect(blogLink.tagName).toEqual("A"); + expect(blogLink).toHaveAttribute("href", "https://yceffort.kr"); + }); + + it("블로그는 같은 창에서 열려야 한다", () => { + const blogLink = screen.getByText("블로그"); + expect(blogLink).not.toHaveAttribute("target"); + }); +});``` + + +```tsx +// 컴포넌트 파트 +
    +
  • + +
  • +
; + +// 테스트 코드 파트 +describe("링크 확인", () => { + it("링크가 3개 존재한다.", () => { + const ul = screen.getByTestId("ul"); + // eslint-disable-next-line + expect(ul.children.length).toBe(3); + }); +}); +```` + +- 데이터 셋을 활용하여 테스트를 좀 더 편리하게 할 수 있다 +- 컴포넌트 파트에서 테스트가 필요한 요소에 data 로 시작하는 HTML 속성을 넣고, getByTestId 메서드를 사용하여 테스트 + +- 아래와 같이 data 를 사용하면 아래와 같은 시나리오에서 더 유용하에 사용 가능 + +- data 셋 적용 전 코드, 모든 button 요소에 onClick 함수를 붙여 주므로 메모리 낭비가 심하다 + +```jsx +export default function App() { + function handleClickButton(index: number) { + return function (_: MouseEvent) { + console.log(index); + }; + } + return ( +
    + {Array.from({ length: 10 }).map((_, index) => ( +
  • + +
  • + ))} +
+ ); +} +``` + +- data 셋 적용 후 코드, 해당 요소의 data-id 속성을 읽어서 리턴하면 되므로 메모리 절약이 가능 + +```jsx +export default function App() { + function handleButtonClick(e: MouseEvent) { + if (e.target instanceof HTMLButtonElement) { + // dataset.id는 해당 요소의 data-id 값이다. + console.log(e.target.dataset.id); + } + } + return ( +
    + {Array.from({ length: 10 }).map((_, index) => ( +
  • + {" "} +
  • + ))} +
+ ); +} +``` + +#### 동적 컴포넌트 + +- 테스트용 컴포넌트 코드 + +```tsx +import { useState } from "react"; + +export function InputComponent() { + const [text, setText] = useState(""); + + function handleInputChange(event: React.ChangeEvent) { + const rawValue = event.target.value; + const value = rawValue.replace(/[^A-Za-z0-9]/gi, ""); + + setText(value); + } + + function handleButtonClick() { + alert(text); + } + + return ( + <> + + + + + ); +} +``` + +- 테스트 코드 + +```tsx +import { act, fireEvent, render } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { InputComponent } from "./InputComponent"; + +describe("InputComponent 테스트", () => { + const setup = () => { + // eslint-disable-next-line + const screen = render(); + const input = screen.getByLabelText("input") as HTMLInputElement; + const button = screen.getByText(/제출하기/i) as HTMLButtonElement; + + return { + input, + button, + ...screen, + }; + }; + + it("input의 초깃값은 빈 문자열이다.", () => { + const { input } = setup(); + + expect(input.value).toEqual(""); + }); + + it("input의 최대 길이가 20자로 설정돼 있다.", () => { + const { input } = setup(); + + expect(input).toHaveAttribute("maxlength", "20"); + }); + + it("영문과 숫자만 입력된다.", () => { + const { input } = setup(); + const inputValue = "안녕하세요123"; + + // 사용자의 입력을 흉내내는 메서드, 사용자가 키보드로 타이핑을 하는 것을 테스트 가능 + userEvent.type(input, inputValue); + + expect(input.value).toEqual("123"); + }); + + it("아이디를 입력하지 않으면 버튼이 활성화되지 않는다.", () => { + const { button } = setup(); + + expect(button).toBeDisabled(); + }); + + it("아이디를 입력하면 버튼이 활성화된다.", () => { + const { button, input } = setup(); + const inputValue = "helloworld"; + + userEvent.type(input, inputValue); + + expect(input.value).toEqual(inputValue); + expect(button).toBeEnabled(); + }); + + it("버튼을 클릭하면 alert가 해당 아이디로 표시된다.", () => { + const alertMock = jest + .spyOn(window, "alert") + .mockImplementation((_: string) => undefined); + + const { button, input } = setup(); + const inputValue = "helloworld"; + + userEvent.type(input, inputValue); + fireEvent.click(button); + + expect(alertMock).toHaveBeenCalledTimes(1); + expect(alertMock).toHaveBeenCalledWith(inputValue); + }); +}); +``` + +- userEvent.type : 사용자가 타이핑하는 것을 흉내 내는 메서드. fireEvent 와는 달리 여러 이벤트를 순차적으로 실행하여 좀 더 자세하게 사용자의 동작을 흉내 낸다. +- fireEvent 로는 단발 동작만 테스트 가능하므로 maxLength 20 을 확인하기 위해서 userEvent 를 사용 + - 단, React 18 버전으로 넘어오면서 약간의 warning 이 생기는 문제 발생 중 +- jest.spyOn : 실행과 관련된 정보만 얻고 싶을 때 사용하는 메서드. 실제로 테스트 메서드에 영향을 미치지 않고 테스트를 할 수 있다 +- mockImplementation : 테스트하고자 하는 메서드의 mocking 을 도와준다. node.js 환경에서 없는 window.alert 에 대한 테스트를 위해 사용 + +- 책에 나온 코드 + +```js +it("영문과 숫자만 입력된다.", () => { + const { input } = setup(); + const inputValue = "안녕하세요123"; + + // 사용자의 입력을 흉내내는 메서드, 사용자가 키보드로 타이핑을 하는 것을 테스트 가능 + userEvent.type(input, inputValue); + + expect(input.value).toEqual("123"); +}); +``` + +- userEvent 사용시 발생하는 Warning 상황 + ![warning 상황](./act_warning.png) + +- React 18 이상 버전에서 실제 문제 없이 작동하는 코드 + +```js +it("영문과 숫자만 입력된다.", () => { + const { input } = setup(); + const inputValue = "안녕하세요123"; + + // fireEvent 로 처리해야 문제가 생기지 않음 + fireEvent.change(input, { target: { value: inputValue } }); + + expect(input.value).toEqual("123"); +}); +``` + +- fireEvent 로 변경 후, 정상 작동하는 상황 + ![정상 상황](./no_warning.png) + +- 아마도, Testing Library 의 경우 테스트 이외의 상황에서 컴포넌트의 변화가 발생하면 테스트 결과가 맞아도 위와 같은 Warining 을 발생하는 것을 확인 가능 +- userEvent 의 경우 fireEvent 의 input 이 여러번 발생하는 구조이므로, setText 가 계속 적으로 컴포넌트를 업데이트 하는 상황이 발생하여 생기는 에러로 추측 +- 따라서 userEvent.type 이 아닌 fireEvent.change 로 한번에 값의 변화를 주고 테스트 하면 정상 테스트가 되는 것을 확인 가능 + + - 아이디어 참고 : https://kentcdodds.com/blog/fix-the-not-wrapped-in-act-warning + - 한번 쯤 보면 매우 좋을듯, 컴포넌트의 모든 상태 변화에 대한 이해를 바탕으로 테스트를 작성해야 한다는 점 + - 실제 사례를 보면, 비동기로 상태를 반영하기 때문에 promise 를 테스트 상황에서 예외 처리하는 것을 볼 수 있다 + + - 실제 해결 사례 내용 : https://github.com/testing-library/react-testing-library/issues/1051 + - React 18 버전으로 업데이트 되면서 생긴 문제라는 것으로 보아 리액트의 변화 사항을 testing-library 가 반영하지 못한 상태로 추측 + +- 문제 해결을 위해 참고한 블로그들 + + - https://flyingsquirrel.medium.com/testing-library-react%EC%9D%98-act%EB%8A%94-%EC%96%B8%EC%A0%9C-%EC%8D%A8%EC%95%BC%ED%95%A0%EA%B9%8C-c6036a8cd4b3 + + - https://kentcdodds.com/blog/common-mistakes-with-react-testing-library + - https://seongry.github.io/2021/06-20-common-mistakes-with-rty/ + - https://kentcdodds.com/blog/fix-the-not-wrapped-in-act-warning + - https://github.com/kentcdodds/fix-act-warning-demos/blob/main/src/__tests__/username-form.js + +#### 비동기 이벤트가 발생하는 컴포넌트 + +- 테스트용 컴포넌트 코드 + +```tsx +import { MouseEvent, useState } from "react"; + +interface TodoResponse { + userId: number; + id: number; + title: string; + completed: false; +} + +export function FetchComponent() { + const [data, setData] = useState(null); + const [error, setError] = useState(null); + async function handleButtonClick(e: MouseEvent) { + const id = e.currentTarget.dataset.id; + const response = await fetch(`/todos/${id}`); + + if (response.ok) { + const result: TodoResponse = await response.json(); + setData(result); + } else { + setError(response.status); + } + } + return ( +
+

{data === null ? "불러온 데이터가 없습니다." : data.title}

+ {error &&

에러가 발생했습니다

} +
    + {Array.from({ length: 10 }).map((_, index) => { + const id = index + 1; + return ( + + ); + })} +
+
+ ); +} +``` + +- 테스트 코드 + +```js +import { fireEvent, render, screen } from "@testing-library/react"; +import { rest } from "msw"; +import { setupServer } from "msw/node"; + +import { FetchComponent } from "."; + +const MOCK_TODO_RESPONSE = { + userId: 1, + id: 1, + title: "delectus aut autem", + completed: false, +}; + +const server = setupServer( + rest.get("/todos/:id", (req, res, ctx) => { + const todoId = req.params.id; + + if (Number(todoId)) { + return res(ctx.json({ ...MOCK_TODO_RESPONSE, id: Number(todoId) })); + } else { + return res(ctx.status(404)); + } + }) +); + +beforeAll(() => server.listen()); +afterEach(() => server.resetHandlers()); +afterAll(() => server.close()); + +beforeEach(() => { + render(); +}); + +describe("FetchComponent 테스트", () => { + it("데이터를 불러오기 전에는 기본 문구가 뜬다.", async () => { + const nowLoading = screen.getByText(/불러온 데이터가 없습니다./); + expect(nowLoading).toBeInTheDocument(); + }); + + it("버튼을 클릭하면 데이터를 불러온다.", async () => { + const button = screen.getByRole("button", { name: /1번/ }); + fireEvent.click(button); + const data = await screen.findByText(MOCK_TODO_RESPONSE.title); + expect(data).toBeInTheDocument(); + }); + + it("버튼을 클릭하고 서버 요청에서 에러가 발생하면 에러 문구를 노출한다.", async () => { + server.use( + rest.get("/todos/:id", (req, res, ctx) => { + return res(ctx.status(503)); + }) + ); + + const button = screen.getByRole("button", { name: /1번/ }); + fireEvent.click(button); + + const error = await screen.findByText(/에러가 발생했습니다/); + expect(error).toBeInTheDocument(); + }); +}); +``` + +- msw 와 CRA 상의 문제로 vitest 사용 필요 +- 참고 할것, https://mswjs.io/docs/migrations/1.x-to-2.x +- 참고한 문서들 + - https://github.com/mswjs/msw/issues/1810 + - https://github.com/react-dnd/react-dnd/issues/3443 + - https://stackoverflow.com/questions/49263429/jest-gives-an-error-syntaxerror-unexpected-token-export + +### 8.2.4 사용자 정의 훅 테스트하기 + +- 훅을 테스트하기 위해서는 컴포넌트에 훅을 삽입하면, 훅 이외의 것을 테스트 하는 환경을 갖춰야 하므로 훅만을 테스트하기 위해서 @testing-library/react-hooks 를 활용 +- 18 이전 버전 에서는 별도로 import 해줘야 하지만, 18 버전 이후로는 @testing-library/react 에 통합 +- renderHook 이라는 메서드로 테스트용 컴포넌트를 가상으로 만들고, 해당 컴포넌트에 대해 훅의 규칙을 위반하는지 테스트한다 + +- useEffectDebugger 코드 + +```tsx +import { useEffect, useRef, DependencyList } from "react"; +export type Props = Record; +export const CONSOLE_PREFIX = "[useEffectDebugger]"; + +export default function useEffectDebugger( + componentName: string, + props?: Props +) { + const prevProps = useRef(); + + useEffect(() => { + if (process.env.NODE_ENV === "production") { + return; + } + + const prevPropsCurrent = prevProps.current; + + if (prevPropsCurrent !== undefined) { + const allKeys = Object.keys({ ...prevProps.current, ...props }); + + const changedProps: Props = allKeys.reduce((result, key) => { + const prevValue = prevPropsCurrent[key]; + const currentValue = props ? props[key] : undefined; + + if (!Object.is(prevValue, currentValue)) { + result[key] = { + before: prevValue, + after: currentValue, + }; + } + + return result; + }, {}); + + if (Object.keys(changedProps).length) { + // eslint-disable-next-line no-console + console.log(CONSOLE_PREFIX, componentName, changedProps); + } + } + + prevProps.current = props; + }); +} +``` + +- useEffectDebbuger 커스텀 훅을 사용하는 컴포넌트 코드 + +```tsx +import { useState } from "react"; +import useEffectDebugger from "./useEffectDebugger"; + +function Test(props: { a: string; b: number }) { + const { a, b } = props; + useEffectDebugger("TestComponent", props); + return ( + <> +
{a}
{b}
+ + ); +} +function TestUseEffectDebugger() { + const [count, setCount] = useState(0); + return ( + <> + {" "} + {" "} + + ); +} +export default TestUseEffectDebugger; +``` + +- 실제 useEffectDubugger 커스텀 훅을 테스트하는 코드 + +```tsx +import { renderHook } from "@testing-library/react"; +import useEffectDebugger, { CONSOLE_PREFIX } from "./useEffectDebugger"; + +const consoleSpy = jest.spyOn(console, "log"); +const componentName = "TestComponent"; + +describe("useEffectDebugger", () => { + afterAll(() => { + // eslint-disable-next-line + // @ts-ignore + process.env.NODE_ENV = "development"; + }); + + it("props 가 없으면 호출되지 않는다", () => { + renderHook(() => useEffectDebugger(componentName)); + + expect(consoleSpy).not.toHaveBeenCalled(); + }); + + it("최초에는 호출되지 않는다", () => { + const props = { hello: "world" }; + + renderHook(() => useEffectDebugger(componentName, props)); + + expect(consoleSpy).not.toHaveBeenCalled(); + }); + + it("props 가 변경되지 않으면 호출되지 않는다", () => { + const props = { hello: "world" }; + + const { rerender } = renderHook(() => + useEffectDebugger(componentName, props) + ); + + expect(consoleSpy).not.toHaveBeenCalled(); + + rerender(); + + expect(consoleSpy).not.toHaveBeenCalled(); + }); + + it("props 가 변경되면 다시 호출한다", () => { + const props = { hello: "world" }; + + const { rerender } = renderHook( + () => useEffectDebugger(componentName, props), + { + initialProps: { + componentName, + props, + }, + } + ); + + const newProps = { hello: "world2" }; + + rerender({ componentName, props: newProps }); + + expect(consoleSpy).not.toHaveBeenCalled(); + }); + + it("process.env.NODE_ENV 가 production 이면 호출되지 않는다", () => { + // eslint-disable-next-line + // @ts-ignore + process.env.NODE_ENV = "production"; + + const props = { hello: "world" }; + + const { rerender } = renderHook( + () => useEffectDebugger(componentName, props), + { + initialProps: { + componentName, + props, + }, + } + ); + + const newProps = { hello: "world2" }; + + rerender({ componentName, props: newProps }); + + expect(consoleSpy).not.toHaveBeenCalled(); + }); +}); +``` + +- 해당 커스텀 훅은 props 가 변경되는 특정 상황에만 console.log 를 띄우는 커스텀 훅이므로 console.log 를 기준으로 테스트를 한다 +- jest 의 spyOn 을 사용하여, 테스트 환경에서 console.log 가 실행되는지를 테스트 + +### 8.2.5 테스트를 작성하기에 앞서 고려해야 할 점 + +- 테스트 커버리지는 만능이 아니다! +- Test Driven Development 로 개발을 하더라도 프론트 엔드는 사용자의 입력을 100% 커버할 수 없기 때문에 모든 상황을 대비하는 테스트 코드 작성은 어렵다 +- 때로는 TDD 대신 QA 를 믿고, 빠르게 개발할 수 밖에 없는 상황이 있다 +- 서비스의 가장 중요한 부분과, 취약점을 파악하고 개발 & 테스트 하는 것이 중요하다 + +### 8.2.6 그 박에 해볼 만한 여러 가지 테스트 + +- 유닛 테스트 : 분리된 환경에서 유닛과 컴포넌트의 동작 테스트 +- 통합 테스트 : 유닛 테스트를 통과한 것들을 묶어서 테스트 +- 엔드 투 엔드 : 실제 동작하는 환경을 가정하고 모든 상황을 테스트 + +\*\* [p. 526] TDD 에 대한 의견은 어떠신가요? 실제로 TDD 기반으로 개발해보신 경험이 있으신가요? diff --git a/pages/6week/xenosign/vite-jest-1.png b/pages/6week/xenosign/vite-jest-1.png new file mode 100644 index 0000000..4c40a0d Binary files /dev/null and b/pages/6week/xenosign/vite-jest-1.png differ diff --git a/pages/6week/xenosign/vite-jest-2.png b/pages/6week/xenosign/vite-jest-2.png new file mode 100644 index 0000000..29b9bf8 Binary files /dev/null and b/pages/6week/xenosign/vite-jest-2.png differ diff --git a/pages/6week/xenosign/vite-jest-3.png b/pages/6week/xenosign/vite-jest-3.png new file mode 100644 index 0000000..20582e9 Binary files /dev/null and b/pages/6week/xenosign/vite-jest-3.png differ diff --git a/pages/6week/xenosign/vite-success.png b/pages/6week/xenosign/vite-success.png new file mode 100644 index 0000000..c0c18c1 Binary files /dev/null and b/pages/6week/xenosign/vite-success.png differ diff --git a/pages/6week/yeonsuu21/study.mdx b/pages/6week/yeonsuu21/study.mdx index 1c8f9aa..8caa180 100644 --- a/pages/6week/yeonsuu21/study.mdx +++ b/pages/6week/yeonsuu21/study.mdx @@ -1 +1,61 @@ -## 정리하기 파일 +# 08. 좋은 리액트 코드 작성을 위한 환경구축하기 + +## 8.1 ESLint를 활용한 정적 코드 분석 + +ESLint : 자바스크립트모드를 정적 분석해 잠재적인 문제를발견하고 나아가 수정까지 도와주는 도구 + +- eslint-plugin : 규칙을 모아놓은 패키지 +- eslint-config : slint-plugin을 한데 묶어서 완벽하게 한 세트로 제공하는 패키지 , 빠르게 적용 + - eslint-config-airbnb : 에어비앤비에서 제작함 , 압도적인 다운로드 수 + - titicaca/triple-config-kit : 유지보수가 활발한 편 + - eslint-config- next : 넥스트 프레임워크를 사용하고 있는 프로젝트에서 사용 + +### 8.1.3 나만의 ESLint 규칙 만들기 + +1. import React를 제거하기 위한 ESLint 규칙 만들기 +2. new Date를 금지시키는 규칙 + +### 8.1.4 주의할점 + +**Prettier와의 충돌** + +: 코드의 포매팅을 도외주는 도구 , 코드를 정적 분석해서 문제를 해결 한다는 점은 동일 + +**규칙에 대한 예외 처리**, 그리고 react-hooks/no-exhaustive-deps + +:일부 코드에서 특정 규칙을 임시로 제외시키고 싶다면 eslint-disable- 주석을 사용 +**ESLint 버전충돌** + +: ESLint couldn’t find the plugin "eslint-plugin-promise’ + +## 8.2 리액트 팀이 권장하는 리액트 테스트 라이브러리 + +### 8.2.1 React Testing Library란? + +리액트를 기반으로 한 테스트를 수행하기 위해 만들어진 것. + +실제로 리액트 컴포넌트를 렌더링하지 않고 ,브라우저를 직접 실행해 눈으로 확인하지 않아도 리액트 컴포넌트가 원하는 대로 렌더링되고 있는지 확인 할 수 있다. + +### 8.2.2 자바스크립트 테스트의 기초 + +1. 테스트할 함수나 모률을 선정한다. +2. 함수나 모률이 반환하길 기대하는 값을 적는다. +3. 함수나 모듈의 실제 반환 값을 적는다. +4. 3번의 기대에 따라 2번의 결과가 일치하는지 확인한다. +5. 기대하는 결과를 반환한다면 테스트는 성공이며, 만약 기대와 다른 결과를 반환하면 에러를 던진다 + + Jest로 테스트코드 작성 + +→ 무엇을 테스트했는지,소요된 시간, 무엇이 성공하고 실패했는지,전체 결과는 어떤지에 대한 정보를 확인가능 + +### 8.2.3 리액트 컴포넌트 테스트 코드 작성하기 + +### 8.2.4 사용자 정의 훅 테스트하기 + +훅이 들어있는 컴포넌트 만들기 → 테스트코드 작성외에 작업이 더 추가됨 + +훅이 들어있는 컴포넌트에 훅에 대한 테스트 만들기 → 해당 훅이 모든 테스트케이스를 커버하지 못할 경우 또 다른 컴포넌트를 찾아야 함 + +⇒ react-hooks-testing-library 로 해결 + +- useEffectDebugger : 디버거 역할 , props가 변경되는 것만 확인할 수 있음 , 부모 컴포넌트가 리렌더링되는 경우에는 useEffectDebugger로 확인할 수 없음 diff --git a/pages/index.mdx b/pages/index.mdx index ea00ca1..74f8867 100644 --- a/pages/index.mdx +++ b/pages/index.mdx @@ -48,8 +48,8 @@ - 2주차 : 이효석, 장경은 - 3주차 : 이설아, 고도연 - 4주차 : 김호준, 이효석 -- 5주차 : 김용현, 김연수 -- 6주차 : 김호준, 고도연 +- 5주차 : 김용현, 고도연 +- 6주차 : 김호준, 김연수 - 7주차 : 이설아, 김은정 - 8주차 : 김용현, 김연수 - 9주차 : 김은정, 김종이 diff --git a/pages/presentation/4week/xenosign.mdx b/pages/presentation/4week/xenosign.mdx index 6c92665..23670ae 100644 --- a/pages/presentation/4week/xenosign.mdx +++ b/pages/presentation/4week/xenosign.mdx @@ -1 +1,540 @@ ## 발표자료 + +### 발표 자료(PPT) 다운로드 링크 + +https://docs.google.com/presentation/d/1NUac4epQuQCVcLtd1R4QnX1Dn0N6A5Rw/edit?usp=sharing&ouid=100598926075562912760&rtpof=true&sd=true + +## 공용 API 코드 + +- api.js + +```js +const BASE_URL = "http://localhost:4000"; + +export async function getTodo() { + const url = `${BASE_URL}/fetch`; + const response = await fetch(url); + return await response.json(); +} + +export async function addTodo(todo) { + const response = await fetch(`${BASE_URL}/post/${todo}`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + }); + + if (!response.ok) { + throw new Error("업로드 실패"); + } + + return await response.json(); +} +``` + +## Redux 로 구현 된 TodoList 어플리케이션 + +### 단점 + +- 모든 로직이 컴포넌트에 집중 +- 비동기 상태 관리를 위한 불필요한 코드 & 변수 +- 서로 다른 로직 사용 시, 협의 필요 -> 협업의 문제 +- 방대해진 Store 코드로 인한 가독성, 리팩토링의 문제 + +### 실제 코드 + +- Todo 컴포넌트 코드(Todo.jsx) + +```jsx +import { useEffect, useRef } from "react"; +import { useDispatch, useSelector } from "react-redux"; + +// redux-store 액션 함수 가져오기 +import { + errorFetch, + errorPost, + requestFetch, + requestPost, + successFetch, + successPost, +} from "./store"; + +// 비동기 통신 api 함수 가져오기 +import { getTodo, addTodo } from "./api"; + +export default function Todo() { + const inputRef = useRef(); + + // redux 사용을 위한 선언 및 상태값 불러오기 + const dispatch = useDispatch(); + const data = useSelector((state) => state.fetchTodo.data); + const fetchIsLoading = useSelector((state) => state.fetchTodo.isLoading); + const fetchError = useSelector((state) => state.fetchTodo.error); + + const postIsLoading = useSelector((state) => state.postTodo.isLoading); + const postError = useSelector((state) => state.postTodo.error); + + // todolist 불러오는 함수 + const fetchTodo = async () => { + try { + dispatch(requestFetch()); + const todoList = await getTodo(); + dispatch(successFetch(todoList)); + } catch (err) { + dispatch(errorFetch(err)); + } + }; + + // todolist 추가하는 함수 + const handleSubmit = async (e) => { + e.preventDefault(); + try { + dispatch(requestPost()); + await addTodo(inputRef.current.value); + dispatch(successPost()); + fetchTodo(); + } catch (err) { + dispatch(errorPost(err)); + } + }; + + // 컴포넌트 마운트 시, todolist 불러오는 useEffect + useEffect(() => { + fetchTodo(); + }, [dispatch]); + + // redux 의 비동기 상태값을 사용한 에러 핸들링 파트 + if (fetchIsLoading || postIsLoading) return

로딩 중

; + + if (fetchError || postError) return

에러 발생

; + + if (data === undefined) return

리스트 없음

; + + // 렌더링 파트 + return ( +
+
    + {data?.map(({ id, content }) => ( +
  • {content}
  • + ))} +
+
+ + +
+
+ ); +} +``` + +- Redux store 코드(store.js) + +```js +import { createStore } from "redux"; + +// 초기 상태 +const initialState = { + fetchTodo: { + data: [], + isLoading: false, + error: undefined, + }, + postTodo: { + isLoading: false, + error: undefined, + }, +}; + +// 액션 타입 정의 +const REQUEST_FETCH = "REQUEST_FETCH"; +const SUCCESS_FETCH = "SUCCESS_FETCH"; +const ERROR_FETCH = "ERROR_FETCH"; + +const REQUEST_POST = "REQUEST_POST"; +const SUCCESS_POST = "SUCCESS_POST"; +const ERROR_POST = "ERROR_POST"; + +// 액션 생성자 +export const requestFetch = () => ({ type: REQUEST_FETCH }); +export const successFetch = (data) => ({ + type: SUCCESS_FETCH, + payload: data, +}); +export const errorFetch = (error) => ({ + type: ERROR_FETCH, + payload: error, +}); + +export const requestPost = () => ({ type: REQUEST_POST }); +export const successPost = () => ({ type: SUCCESS_POST }); +export const errorPost = (error) => ({ + type: ERROR_POST, + payload: error, +}); + +// 리듀서 +const todoReducer = (state = initialState, action) => { + switch (action.type) { + case REQUEST_FETCH: + return { + ...state, + fetchTodo: { + data: undefined, + isLoading: true, + error: undefined, + }, + }; + case SUCCESS_FETCH: + return { + ...state, + fetchTodo: { + data: action.payload, + isLoading: false, + error: undefined, + }, + }; + case ERROR_FETCH: + return { + ...state, + fetchTodo: { + data: undefined, + isLoading: false, + error: action.payload, + }, + }; + case REQUEST_POST: + return { + ...state, + postTodo: { + isLoading: true, + error: undefined, + }, + }; + case SUCCESS_POST: + return { + ...state, + postTodo: { + isLoading: false, + error: undefined, + }, + }; + case ERROR_POST: + return { + ...state, + postTodo: { + isLoading: false, + error: action.payload, + }, + }; + default: + return state; + } +}; + +// 스토어 생성 +const store = createStore(todoReducer); + +export default store; +``` + +## Redux-thunk 를 적용한 코드 + +### 개선점 + +- thunk 의 적용으로 dispatch 에 함수 자체를 전달이 가능 -> Store 에 선언한 함수를 그대로 전달이 가능 +- API 호출 통신 로직을 컴포넌트에서 분리 +- 타 컴포넌트에서도 Store 의 통신 로직을 불러와서 바로 사용 가능 +- 별도의 상태 관리 로직도 Store 에서 통합 관리 + +### 남아있는 단점 + +- Store 가 더 방대해짐 -> 낮아진 가독성, 리팩토링의 문제점 + +### 코드 + +- Todo 컴포넌트 코드(Todo.jsx) + +```jsx +import { useEffect, useRef } from "react"; +import { useDispatch, useSelector } from "react-redux"; +import { + errorFetch, + errorPost, + fetchTodo, + postTodo, + requestFetch, + requestPost, + successFetch, + successPost, +} from "./store"; + +export default function Todo() { + const inputRef = useRef(); + + const dispatch = useDispatch(); + const data = useSelector((state) => state.fetchTodo.data); + const fetchIsLoading = useSelector((state) => state.fetchTodo.isLoading); + const fetchError = useSelector((state) => state.fetchTodo.error); + + const postIsLoading = useSelector((state) => state.postTodo.isLoading); + const postError = useSelector((state) => state.postTodo.error); + + // 통신 로직 자체를 Store 에서 불러서 쓰므로, 컴포넌트 코드 자체가 단순해짐 + useEffect(() => { + fetchTodo(); + }, [dispatch]); + + useEffect(() => { + dispatch(fetchTodo()); + }, [dispatch]); + + const handleSubmit = (e) => { + dispatch(postTodo(inputRef.current.value)); + }; + + if (fetchIsLoading || postIsLoading) return

로딩 중

; + + if (fetchError || postError) return

에러 발생

; + + if (data === undefined) return

리스트 없음

; + + return ( +
+
    + {data?.map(({ id, content }) => ( +
  • {content}
  • + ))} +
+
+ + +
+
+ ); +} +``` + +- Redux Store 코드(store.js) + +```js +import { createStore, applyMiddleware } from "redux"; +import { thunk } from "redux-thunk"; // redux-thunk 불러오기 +import { getTodo, addTodo } from "./api"; // API 호출 함수 불러오기 + +// 초기 상태 정의 +const initialState = { + fetchTodo: { + data: [], + isLoading: false, + error: undefined, + }, + postTodo: { + isLoading: false, + error: undefined, + }, +}; + +// 액션 타입 정의 +const REQUEST_FETCH = "REQUEST_FETCH"; +const SUCCESS_FETCH = "SUCCESS_FETCH"; +const ERROR_FETCH = "ERROR_FETCH"; + +const REQUEST_POST = "REQUEST_POST"; +const SUCCESS_POST = "SUCCESS_POST"; +const ERROR_POST = "ERROR_POST"; + +// 액션 생성자 +export const requestFetch = () => ({ type: REQUEST_FETCH }); +export const successFetch = (data) => ({ + type: SUCCESS_FETCH, + payload: data, +}); +export const errorFetch = (error) => ({ + type: ERROR_FETCH, + payload: error, +}); + +export const requestPost = () => ({ type: REQUEST_POST }); +export const successPost = () => ({ type: SUCCESS_POST }); +export const errorPost = (error) => ({ + type: ERROR_POST, + payload: error, +}); + +// 컴포넌트에서 사용하던 통신 로직 파트를 Store 에서 구현 +// dispatch 를 통해 함수 자체를 전달하는 구조 (thunk 가 없으면 객체만 전달이 가능) +export const fetchTodo = () => { + return async (dispatch) => { + dispatch(requestFetch()); + try { + const data = await getTodo(); + dispatch(successFetch(data)); + } catch (error) { + dispatch(errorFetch(error)); + } + }; +}; + +export const postTodo = (content) => { + return async (dispatch) => { + dispatch(requestPost()); + try { + await addTodo(content); + dispatch(successPost()); + dispatch(fetchTodo()); + } catch (error) { + dispatch(errorPost(error)); + } + }; +}; + +// 리듀서 +const todoReducer = (state = initialState, action) => { + switch (action.type) { + case REQUEST_FETCH: + return { + ...state, + fetchTodo: { + data: undefined, + isLoading: true, + error: undefined, + }, + }; + case SUCCESS_FETCH: + return { + ...state, + fetchTodo: { + data: action.payload, + isLoading: false, + error: undefined, + }, + }; + case ERROR_FETCH: + return { + ...state, + fetchTodo: { + data: undefined, + isLoading: false, + error: action.payload, + }, + }; + case REQUEST_POST: + return { + ...state, + postTodo: { + isLoading: true, + error: undefined, + }, + }; + case SUCCESS_POST: + return { + ...state, + postTodo: { + isLoading: false, + error: undefined, + }, + }; + case ERROR_POST: + return { + ...state, + postTodo: { + isLoading: false, + error: action.payload, + }, + }; + default: + return state; + } +}; + +// 스토어 생성 +const store = createStore(todoReducer, applyMiddleware(thunk)); + +export default store; +``` + +## Redux-query 를 적용한 코드 + +### 개선점 + +- 컴포넌트 상태 관리와 서버 상태 관리를 분리 가능 +- 통신 파트를 개발자가 직접 구현할 필요 없이 react-query 의 기능으로 사용 +- 안정적인 라이브러리 사용으로 별도의 테스트 필요 없음 +- 서버 상태 관리를 위한 불필요한 코드 및 Redux 의 Boilerplate 코드 대폭 감소 + - 컴포넌트 코드 : 80 줄 -> 60줄 + - 서버 상태 관리를 위한 Redux Store 코드 : 132줄 -> 0줄 + +### 코드 + +- Todo 컴포넌트 코드(TodoQuery.jsx) + +```jsx +import { useEffect, useRef } from "react"; +import { useDispatch, useSelector } from "react-redux"; +import { + errorFetch, + errorPost, + fetchTodo, + postTodo, + requestFetch, + requestPost, + successFetch, + successPost, +} from "./store"; +import { getTodo, addTodo } from "./api"; + +// React Query +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; + +export default function TodoQuery() { + const inputRef = useRef(); + const queryClient = useQueryClient(); + + const { data, isLoading, isError } = useQuery({ + queryKey: ["todo"], + queryFn: () => getTodo(), + }); + + const postTodoMutation = useMutation({ + mutationFn: (todo) => { + addTodo(todo); + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["todo"] }); + }, + }); + + const handleSubmit = (e) => { + e.preventDefault(); + + postTodoMutation.mutate(inputRef.current.value, { + onSuccess: () => alert("Todo 등록 성공"), + onError: () => alert(" 등록 실패"), + }); + + inputRef.current.value = ""; + queryClient.invalidateQueries(); + }; + + if (isLoading) return

로딩 중

; + + if (isError) return

에러 발생

; + + if (data === undefined) return

리스트 없음

; + + return ( +
+
    + {data?.map(({ id, content }) => ( +
  • {content}
  • + ))} +
+
+ + +
+
+ ); +} +```