diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 72c1ff7..ce79864 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,5 +1,20 @@ -**Participant** +## 리뷰 요청 사항 +[어떤 부분에 대해 코드 리뷰를 요청하고 싶은지 명확하게 작성해주세요.] +## 리뷰 내용 +[코드 리뷰를 받고자 하는 코드에 대한 설명을 작성해주세요. 어떤 기능을 구현하려고 하는지, 어떤 문제를 해결하려는지 등을 포함시켜주세요.] + +## 주요 변경 사항 +[주요하게 변경한 부분이나 수정한 내용을 간략히 설명해주세요.] + +## 특별히 확인해야 할 부분 +[리뷰어에게 특별히 확인해야 할 부분이 있다면 작성해주세요. 예를 들어, 성능 개선, 보안 이슈, 코드 스타일 등을 확인해야 할 수 있습니다.] + +## 참고 사항 +[리뷰어에게 알려주고 싶은 추가적인 정보나 참고 사항이 있다면 작성해주세요.] + + +## 참여 - [ ] @functionBee - [ ] @HYEOK999 - [ ] @hyjoong diff --git a/chapter03/b.md b/chapter03/b.md new file mode 100644 index 0000000..6692efc --- /dev/null +++ b/chapter03/b.md @@ -0,0 +1,612 @@ +## 03장: 리액트 훅 깊게 살펴보기 + +
+ +- [03장: 리액트 훅 깊게 살펴보기](#03장-리액트-훅-깊게-살펴보기) +- [3.1 리액트의 모든 훅 파헤치기](#31-리액트의-모든-훅-파헤치기) + - [3.1.1 `useState`](#311-usestate) + - [게으른 초기화(lazy initialization)](#게으른-초기화lazy-initialization) + - [`useState` 훅의 규칙](#usestate-훅의-규칙) + - [3.1.2 `useEffect`](#312-useeffect) + - [3.1.3 `useMemo`](#313-usememo) + - [3.1.4 `useCallback`](#314-usecallback) + - [3.1.5 `useRef`](#315-useref) + - [3.1.6 `useContext`](#316-usecontext) + - [3.1.7 `useReducer`](#317-usereducer) + - [3.1.8 `useImperativeHandle`](#318-useimperativehandle) + - [3.1.9 `useLayoutEffect`](#319-uselayouteffect) + - [3.1.10 `useDebugValue`](#3110-usedebugvalue) + - [3.1.11 훅 규칙(Rules of hooks)](#3111-훅-규칙rules-of-hooks) + - [3.1.12 정리](#3112-정리) +- [3.2 사용자 정의 훅과 고차 컴포넌트 중 무엇을 써야 할까?](#32-사용자-정의-훅과-고차-컴포넌트-중-무엇을-써야-할까) + - [3.2.1 사용자 정의 훅(Custom Hook)](#321-사용자-정의-훅custom-hook) + - [3.2.2 고차 컴포넌트(Higher Order Component;HOC)](#322-고차-컴포넌트higher-order-componenthoc) + - [3.2.3 사용자 정의 훅과 고차 컴포넌트 중 무엇을 써야 할까?](#323-사용자-정의-훅과-고차-컴포넌트-중-무엇을-써야-할까) + - [사용자 정의 훅이 필요한 경우](#사용자-정의-훅이-필요한-경우) + - [고차 컴포넌트를 사용해야 하는 경우](#고차-컴포넌트를-사용해야-하는-경우) + - [3.2.4 정리](#324-정리) +- [References](#references) +- [Articles](#articles) + +
+ +## 3.1 리액트의 모든 훅 파헤치기 + +| 훅 이름 | 정의 | 예제 | +| ------------------- | ----------------------------------------------------------------- | ------------------------------------------------------------------------------------ | +| useState | 상태 값을 관리하고 업데이트하는 데 사용 | `const [count, setCount] = useState(0);` | +| useEffect | 컴포넌트의 생명주기에 따른 작업을 수행하는 데 사용 | `useEffect(() => { console.log('Component mounted'); }, []);` | +| useMemo | 계산 결과를 기억하고 재사용하는 데 사용 | `const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);` | +| useCallback | 콜백 함수를 기억하고 재사용하는 데 사용 | `const memoizedCallback = useCallback(() => { /* callback logic */ }, [dependency])` | +| useRef | 컴포넌트 내에서 변경 가능한 값을 저장하는 데 사용 | `const refContainer = useRef(initialValue);` | +| useContext | React의 컨텍스트를 사용하는 데 사용 | `const value = useContext(MyContext);` | +| useReducer | 상태와 액션을 받아 새로운 상태를 반환하는 리듀서 함수와 함께 사용 | `const [state, dispatch] = useReducer(reducer, initialState);` | +| useImperativeHandle | 부모 컴포넌트에서 자식 컴포넌트의 인스턴스를 조작하는 데 사용 | `useImperativeHandle(ref, () => ({ method() { /* method logic */ } }));` | +| useLayoutEffect | DOM 업데이트 이후 동기적으로 작업을 수행하는 데 사용 | `useLayoutEffect(() => { /* layout effect logic */ }, []);` | +| useDebugValue | 커스텀 훅에서 디버깅 정보를 제공하는 데 사용 | `useDebugValue(value);` | + +### 3.1.1 `useState` + +```jsx +/** + * + * 상태와 상태를 갱신하는 함수를 반환하는 훅입니다. + * @param initialState — 초기 상태 값입니다. + */ +useState(initialState); +``` + +```jsx +const [현재 상태, value를 업데이트하는 함수] = useState(초기값); +``` + +- `useState`는 React 함수 컴포넌트에서 상태(state)를 관리하기 위한 훅(hook)입니다. +- `useState`를 사용할 때는 상태의 초기값을 설정하기 위해 매개변수로 전달합니다. + + - 이 훅을 호출하면 배열이 반환되는데, 이 배열의 첫 번째 원소는 현재 상태 값을 나타내고, 두 번째 원소는 해당 상태 값을 업데이트하는 함수인 'Setter 함수'입니다. + - useState 훅을 사용할 때 상태 변수와 그 상태를 업데이트하는 함수에 대한 명명 규칙은 배열 구조 분해를 통해 `[something, setSomething]`과 같이 이름을 지정하는 것입니다. + +
+ 배열 비구조화 할당 + 배열 비구조화 할당은 JavaScript ES6에 도입된 기능으로, 배열의 요소들을 변수에 쉽게 할당할 수 있도록 해주는 문법입니다. 이 방법을 사용하면 배열 내부의 요소를 개별 변수에 할당하기 위해 인덱스를 사용할 필요가 없어 코드가 더 간결하고 읽기 쉬워집니다. + + 예를 들어, 다음과 같은 배열이 있다고 가정해 보겠습니다: + + ```javascript + let numbers = [1, 2, 3]; + ``` + + 전통적인 방식으로는 각각의 요소를 변수에 할당하기 위해 다음과 같이 작성해야 합니다: + + ```javascript + let one = numbers[0]; + let two = numbers[1]; + let three = numbers[2]; + ``` + + 하지만 배열 비구조화 할당을 사용하면 다음과 같이 훨씬 간단하게 작성할 수 있습니다: + + ```javascript + let [one, two, three] = numbers; + ``` + + 이렇게 하면 numbers 배열의 첫 번째 요소가 one 변수에, 두 번째 요소가 two 변수에, 세 번째 요소가 three 변수에 각각 할당됩니다. + + 또한 배열 비구조화 할당은 기본값 설정, 나머지 요소(rest elements) 수집 등의 추가적인 기능도 제공합니다. 예를 들어, 다음과 같은 경우를 살펴보겠습니다: + + ```javascript + let [one, two, three, four = 4] = numbers; + ``` + + 위 코드에서 numbers 배열에는 세 개의 요소만 존재하지만, four 변수에는 기본값으로 4가 할당됩니다. + + 나머지 요소를 수집하는 예시는 다음과 같습니다: + + ```javascript + let [one, ...rest] = numbers; + ``` + + rest 변수는 배열의 나머지 부분을 담게 되며, 이 경우 [2, 3]이 됩니다. +
+ +- 리액트의 함수형 컴포넌트는 매 렌더링마다 새로운 함수 호출로 간주되기 때문에, 컴포넌트의 로컬 변수들은 호출이 끝날 때 사라지고 다음 렌더링 때 새로 생성됩니다. 하지만 `useState`와 같은 훅을 사용하면, 리액트가 내부적으로 상태를 관리하여 컴포넌트가 다시 실행되어도 이전 상태 값을 유지할 수 있게 합니다. + - 이 상태 관리는 클로저와 밀접한 관련이 있습니다. 클로저는 함수가 선언될 때의 렉시컬 환경을 기억하는 함수를 말합니다. React에서 클로저는 주로 이벤트 핸들러나 비동기 작업에서 이전 상태 값을 참조할 때 중요한 역할을 합니다. + +```jsx +import React, { useState } from "react"; + +function Counter() { + // 'count' 상태와 이 상태를 업데이트하는 'setCount' 함수를 초기화합니다. + // 초기값으로 0을 설정합니다. + const [count, setCount] = useState(0); + + // 카운트를 증가시키는 핸들러 함수입니다. + // 'setCount' 함수에 현재 상태를 기반으로 다음 상태를 결정하는 함수를 전달합니다. + // 이는 상태 업데이트가 비동기적으로 일어나기 때문에 중요합니다. + const handleIncrement = () => { + setCount((prevCount) => prevCount + 1); // 이전 상태값을 사용하여 다음 상태값을 계산합니다. + }; + + // 카운트를 감소시키는 핸들러 함수입니다. + // 여기서도 'setCount'에 함수를 전달하여 상태 업데이트를 안전하게 수행합니다. + const handleDecrement = () => { + setCount((prevCount) => prevCount - 1); // 이전 상태값을 사용하여 다음 상태값을 계산합니다. + }; + + // 'handleIncrement'와 'handleDecrement' 함수는 컴포넌트가 렌더링될 때마다 새로 생성됩니다. + // 그러나 이 함수들은 'setCount'를 통해 상태를 업데이트할 때 현재의 'count' 상태 값을 직접 참조하지 않습니다. + // 대신, 'setCount'에 전달된 이전 상태값(prevCount)을 기반으로 새로운 상태값을 계산합니다. + return ( +
+

현재 카운트: {count}

+ + +
+ ); +} + +export default Counter; +``` + +> 위 코드는 `useState`훅을 사용하여 간단한 카운터 컴포넌트를 구현한 것입니다. `useState`로부터 얻은 `setCount` 함수를 사용할 때, 현재 상태값을 직접 사용하는 대신에 이전 상태값에 기반하여 업데이트를 수행하는 콜백 함수를 전달함으로써, 상태 업데이트가 비동기적으로 일어나더라도 안정적으로 현재 상태값을 계산할 수 있습니다. 이 방식은 특히 여러 상태 업데이트가 동시에 발생할 때 유용하며, 잠재적인 동시성 문제를 방지할 수 있습니다. +> +> **잠재적인 동시성 문제** +> +> - **비동기적 상태 업데이트**: React가 상태 업데이트를 비동기적으로 처리할 수 있기 때문에, count 값이 예상과 다르게 변경될 수 있습니다. 이전 상태를 기반으로 하는 함수를 전달함으로써, 항상 올바른 참조값을 기반으로 상태를 업데이트할 수 있습니다. +> - **배치 처리된 상태 업데이트**: React는 성능 최적화를 위해 여러 setState 호출을 배치 처리할 수 있습니다. 이 경우, 여러 업데이트가 하나의 렌더링 사이클로 합쳐질 수 있으며, 각 업데이트는 독립적인 count 값을 가지지 않고 마지막 상태값을 기준으로 처리됩니다. 콜백 함수를 사용하면 이러한 문제를 방지할 수 있습니다. +> - **클로저에 의한 값의 '스냅샷'**: 함수형 업데이트를 사용하지 않으면, 이벤트 핸들러 내의 count 상태는 해당 함수가 생성될 당시의 값으로 고정됩니다. 이로 인해 실제 DOM 이벤트가 처리될 때 count의 최신 값이 아닌 오래된 값을 참조하는 문제가 발생할 수 있습니다. + +```jsx +import React, { useState, useEffect } from "react"; + +function AutoCounter() { + // 카운트 상태를 초기화합니다. + const [count, setCount] = useState(0); + + // 컴포넌트가 마운트되면 setInterval을 설정합니다. + useEffect(() => { + // 매 초마다 카운트를 증가시키는 interval을 설정합니다. + const intervalId = setInterval(() => { + // setCount 함수에 현재 상태를 기반으로 다음 상태를 결정하는 함수를 전달합니다. + setCount((prevCount) => prevCount + 1); + }, 1000); // 1000ms = 1초 + + // 컴포넌트가 언마운트되거나 재렌더링되기 전에 interval을 정리합니다. + return () => clearInterval(intervalId); + }, []); // 의존성 배열이 비어 있으므로 이 effect는 마운트될 때 딱 한 번만 실행됩니다. + + return ( +
+

자동 카운트: {count}

+
+ ); +} + +export default AutoCounter; +``` + +> 이 코드에서 `useEffect`의 의존성 배열이 빈 배열(`[]`)로 설정되어 있기 때문에, `useEffect` 내의 콜백 함수는 컴포넌트가 처음 마운트될 때 단 한 번만 실행됩니다. `setInterval`에 의해 호출되는 콜백 내에서 `setCount`는 이전 상태 값을 기반으로 새로운 상태 값을 계산하기 때문에, 실제 `count` 값이 어떻게 변화하든 항상 최신 상태를 기준으로 1을 더하게 됩니다. 따라서 카운터는 매 초마다 정확하게 `1`씩 증가합니다. + +#### 게으른 초기화(lazy initialization) + +컴포넌트의 상태를 초기화할 때 초기 상태 계산이 복잡하거나 계산 비용이 높을 경우에 사용되는 기법입니다. `useState` 훅을 사용할 때, 초기 상태를 함수로 전달함으로써 컴포넌트가 처음 렌더링될 때만 해당 상태의 초기값을 계산하도록 할 수 있습니다. + +```jsx +import React, { useState } from "react"; + +function ExpensiveInitialStateComponent() { + // 상태의 초기값을 설정하는 함수를 useState에 전달합니다. + // 이 함수는 컴포넌트가 처음 렌더링될 때 한 번만 호출됩니다. + const [expensiveState, setExpensiveState] = useState(() => { + console.log("이 로그는 컴포넌트가 처음 렌더링될 때만 나타납니다."); + // 복잡한 계산이나 데이터 추출 로직을 여기에 작성합니다. + // 예를 들어, 큰 배열을 생성하거나, 복잡한 연산을 수행하는 등의 작업을 할 수 있습니다. + const initialState = performExpensiveCalculation(); + return initialState; + }); + + // ... + + return
{/* 렌더링 로직 */}
; +} + +function performExpensiveCalculation() { + // 복잡하고 시간이 많이 걸리는 계산을 수행하는 함수입니다. + // 실제로는 여기에 실제 계산 로직이 들어갑니다. + console.log("비싼 계산을 수행합니다."); + return "계산된 초기 상태"; +} + +export default ExpensiveInitialStateComponent; +``` + +> 이 코드에서 `useState`는 초기 상태를 계산하기 위한 함수를 인자로 받습니다. 이 함수는 컴포넌트가 처음 렌더링될 때 한 번만 호출되고, 그 결과값은 상태의 초기값으로 사용됩니다. 이렇게 함으로써 불필요한 계산을 매 렌더링마다 반복하는 것을 방지할 수 있어 성능을 최적화할 수 있습니다. + +#### `useState` 훅의 규칙 + +```jsx +import React, { useState } from "react"; + +function MyComponent() { + // 최상위 레벨에서 useState를 호출하여 'count' 상태 변수를 선언하고 초기값을 0으로 설정합니다. + const [count, setCount] = useState(0); + + // 컴포넌트의 나머지 부분... +} +``` + +- 컴포넌트의 최상위 레벨에서 `useState`를 호출하여 상태 변수를 선언해야 합니다. + - `useState`는 반드시 함수의 최상위에서만 호출되어야 하며, 조건문, 반복문, 중첩된 함수 내부에서 호출되어서는 안 됩니다. 이 규칙은 React의 훅이 항상 동일한 순서로 호출되어야 하는 '훅 규칙'을 따르기 위함입니다 +- `useState`는 함수 컴포넌트 내에서만 호출되어야 합니다. + - 클래스 컴포넌트에서는 `useState`를 사용할 수 없습니다. + +### 3.1.2 `useEffect` + +```jsx +/** + * + * 부수 효과를 포함하는 명령형 코드를 포함하는 함수를 받습니다. + * @param effect — 정리(clean-up) 함수를 반환할 수 있는 명령형 함수입니다. + * @param deps — 제공된 값이 변경될 때만 효과가 활성화됩니다. + */ +useEffect( + () => { + /** + * 이 함수는 컴포넌트가 렌더링될 때마다, + * 그리고 의존성 중 어느 것이든 변경될 때마다 호출됩니다. + */ + return () => { + // clean-up(optional) + }; + }, + [dependencies] // 이 useEffect 훅에 대한 의존성 배열입니다. + // 의존성이 변경되면, useEffect에 전달된 함수가 다시 호출됩니다. +); +``` + +- **`clean-up function`**: + - 이 클린업 함수는 훅에서 생성된 리소스나 효과를 정리하는 데 사용됩니다. + - API에서 데이터를 가져오거나 이벤트 리스너를 설정하거나 간격이나 타임아웃을 생성하는 등의 부작용을 정리할 수 있습니다. + +### 3.1.3 `useMemo` + +```jsx +/** + * + * 계산된 값을 기억하는 데 사용되는 훅입니다. + * @param factory — 새로운 값을 계산하는 함수입니다. + * @param deps — 제공된 값이 변경될 때만 새로운 값을 다시 계산합니다. + */ +useMemo( + () => { + /** + * 이 함수는 의존성이 변경될 때마다 호출되며, + * 새로운 값을 계산하여 반환합니다. + */ + }, + [dependencies] // 이 useMemo 훅에 대한 의존성 배열입니다. + // 의존성이 변경되면, useMemo에 전달된 함수가 다시 호출되고 + // 새로운 값을 반환합니다. +); +``` + +### 3.1.4 `useCallback` + +```jsx +/** + * + * 이전에 생성된 콜백 함수를 기억하는 데 사용되는 훅입니다. + * @param callback — 새로운 콜백 함수를 생성하는 함수입니다. + * @param deps — 제공된 값이 변경될 때만 새로운 콜백 함수를 다시 생성합니다. + */ +useCallback( + () => { + /** + * 이 함수는 의존성이 변경될 때마다 호출되며, + * 새로운 콜백 함수를 생성하여 반환합니다. + */ + }, + [dependencies] // 이 useCallback 훅에 대한 의존성 배열입니다. + // 의존성이 변경되면, useCallback에 전달된 함수가 다시 호출되고 + // 새로운 콜백 함수를 반환합니다. +); +``` + +### 3.1.5 `useRef` + +```jsx +/** + * + * 변경 가능한 값을 유지하는 데 사용되는 훅입니다. + * @param initialValue — 초기 값으로 사용할 값입니다. + */ +useRef(initialValue); +``` + +### 3.1.6 `useContext` + +```jsx +/** + * + * 컨텍스트(Context) 값을 반환하는 훅입니다. + * @param context — 컨텍스트 객체입니다. + */ +useContext(context); +``` + +### 3.1.7 `useReducer` + +```jsx +/** + * + * 상태와 상태를 갱신하는 함수를 반환하는 훅입니다. + * @param reducer — 상태 갱신을 처리하는 리듀서 함수입니다. + * @param initialState — 초기 상태 값입니다. + * @param initializer — 초기 상태를 설정하는 함수입니다. 선택적 매개변수입니다. + */ +useReducer(reducer, initialState, initializer); +``` + +### 3.1.8 `useImperativeHandle` + +```jsx +/** + * + * 외부에서 접근 가능한 인스턴스의 특정 함수를 정의하는 훅입니다. + * @param ref — 외부에서 접근 가능한 인스턴스의 ref 객체입니다. + * @param createHandle — 외부에서 접근 가능한 인스턴스에 정의할 함수를 생성하는 함수입니다. + * @param deps — 제공된 값이 변경될 때만 새로운 핸들을 생성합니다. + */ +useImperativeHandle(ref, createHandle, deps); +``` + +### 3.1.9 `useLayoutEffect` + +```jsx +/** + * + * 명령형 코드를 포함하는 함수를 받아 렌더링 이후에 동기적으로 실행되는 훅입니다. + * @param effect — 정리(clean-up) 함수를 반환할 수 있는 명령형 함수입니다. + * @param deps — 제공된 값이 변경될 때만 효과가 활성화됩니다. + */ +useLayoutEffect( + () => { + /** + * 이 함수는 컴포넌트가 렌더링된 직후에 동기적으로 호출됩니다. + * 의존성 중 어느 것이든 변경될 때마다 호출됩니다. + */ + }, + [dependencies] // 이 useLayoutEffect 훅에 대한 의존성 배열입니다. + // 의존성이 변경되면, useLayoutEffect에 전달된 함수가 다시 호출됩니다. +); +``` + +### 3.1.10 `useDebugValue` + +```jsx +/** + * + * 디버깅 목적으로 사용되는 훅입니다. + * @param value — 디버그할 값입니다. + * @param formatter — 값의 표시 형식을 지정하는 함수입니다. 선택적 매개변수입니다. + */ +useDebugValue(value, formatter); +``` + +### 3.1.11 훅 규칙(Rules of hooks) + +- React 훅(Hooks) 규칙은 React 함수 컴포넌트에서 훅을 사용할 때 지켜야 하는 원칙들입니다. +- React 개발 팀은 이 규칙들을 준수하도록 돕기 위해 ESLint 플러그인인 [eslint-plugin-react-hooks](https://www.npmjs.com/package/eslint-plugin-react-hooks)를 제공하고 있으며, 이를 사용하면 위반 사항을 쉽게 찾아낼 수 있습니다 + - [eslint-plugin-react-hooks](https://www.npmjs.com/package/eslint-plugin-react-hooks) 설치: + ```bash + npm install eslint-plugin-react-hooks@next + ``` + ```json + // ESLint config 파일 + { + "plugins": [ + // ... + "react-hooks" + ], + "rules": { + // ... + "react-hooks/rules-of-hooks": "error" + } + } + ``` +- Hooks 규칙을 어길 경우에는 에러가 발생할 수 있습니다. + ```bash + # console + Hooks can only be called inside the body of a function component. + ``` + +**훅 규칙(Rules of hooks)** + +- Hooks는 함수형 컴포넌트 내에서만 사용되어야 합니다. 대신, 커스텀 훅을 만들어 사용할 수 있습니다. + - 커스텀 Hooks는 `use`로 시작되어야 합니다. 예를 들어, `useCounter`, `useFetch` 등과 같이 이름을 지정해야 합니다. + - 클래스 컴포넌트에서는 Hooks를 사용할 수 없습니다. +- Hooks는 항상 최상위 레벨에서 호출되어야 합니다. 조건문, 반복문, 중첩 함수 내에서는 Hooks를 호출할 수 없습니다. 이는 훅의 호출 순서를 보장하기 위함입니다. + - Hooks는 컴포넌트의 동일한 순서로 호출되어야 합니다. Hooks는 호출 순서에 의존하기 때문에, 조건문 안에서 Hooks를 사용하면 예상치 못한 동작을 할 수 있습니다. + +```jsx +// ✅ Good: 함수 컴포넌트 내에서 훅을 최상위 레벨에서 호출하는 경우 +import React, { useState } from "react"; + +function Counter() { + const [count, setCount] = useState(0); + + const increment = () => { + setCount(count + 1); + }; + + return ( +
+

Count: {count}

+ +
+ ); +} + +export default Counter; +``` + +```jsx +// ✅ Good: 최상위 레벨에서 커스텀 훅을 호출하는 경우 +import React, { useState, useEffect } from "react"; + +function useWindowWidth() { + const [width, setWidth] = useState(window.innerWidth); + + useEffect(() => { + const handleResize = () => { + setWidth(window.innerWidth); + }; + + window.addEventListener("resize", handleResize); + + return () => { + window.removeEventListener("resize", handleResize); + }; + }, []); + + return width; +} + +function MyComponent() { + const windowWidth = useWindowWidth(); + + return

Window Width: {windowWidth}

; +} + +export default MyComponent; +``` + +**훅 규칙(Rules of hooks)이 만들어진 이유** + +1. **훅 호출 순서의 보장**: React는 훅이 호출되는 순서에 의존하여 내부 상태를 관리합니다. 만약 조건문이나 반복문 내에서 훅을 호출한다면, 훅의 호출 순서가 보장되지 않아 React가 내부 상태를 올바르게 관리하지 못할 수 있습니다. +2. **컴포넌트 간의 코드 재사용 촉진**: 훅 규칙을 따르면 커스텀 훅을 만들어 다른 컴포넌트 간에 상태 로직을 쉽게 재사용할 수 있습니다. +3. **버그 예방**: 규칙을 준수하면 훅의 잘못된 사용으로 인한 버그 발생 가능성을 줄일 수 있습니다. +4. **코드의 이해와 유지보수 용이**: 모든 훅이 컴포넌트 최상단에서 호출되어야 한다는 규칙은 코드의 일관성을 유지하고, 다른 개발자들이 코드를 이해하고 유지보수하기 쉽게 만듭니다. + +### 3.1.12 정리 + +
+ +## 3.2 사용자 정의 훅과 고차 컴포넌트 중 무엇을 써야 할까? + +- Custom Hook과 Higher Order Component(HOC)는 React에서 재사용 가능한 로직을 공유하기 위한 두 가지 다른 패턴입니다. + +### 3.2.1 사용자 정의 훅(Custom Hook) + +- 사용자 정의 훅(Custom Hook)은 React에서 재사용 가능한 로직을 추상화하는 기법 중 하나입니다. + + - 예를 들어, HTTP 요청을 하는 fetch를 기반으로 한 사용자 정의 훅인 `useFetch`를 만들 수 있습니다. 이 훅은 내부에 `useState와` `useEffect와` 같은 리액트 훅을 사용하여 로직을 구현합니다. + - 이렇게 사용자 정의 훅을 만들면, 사용하는 쪽에서는 `useFetch`만 사용하여 중복된 로직을 손쉽게 관리할 수 있습니다. + + ```jsx + import { useState, useEffect } from "react"; + + /** + * HTTP 요청을 하는 fetch를 기반으로 한 사용자 정의 훅인 useFetch를 만들 수 있습니다. + * 이 훅은 내부에 useState와 useEffect와 같은 리액트 훅을 사용하여 로직을 구현합니다. + * + * @param {string} url - 데이터를 가져올 URL + * @returns {{ data: any, loading: boolean }} - 데이터와 로딩 상태를 반환하는 객체 + */ + const useFetch = (url) => { + const [data, setData] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + /** + * 데이터를 가져오는 비동기 함수 + */ + const fetchData = async () => { + try { + const response = await fetch(url); + const jsonData = await response.json(); + setData(jsonData); + setLoading(false); + } catch (error) { + console.error(error); + } + }; + + fetchData(); + }, [url]); + + return { data, loading }; + }; + + export default useFetch; + ``` + +- 사용자 정의 훅(Custom Hook)은 `use`라는 접두사로 시작하는 함수로, 여러 컴포넌트에서 사용할 수 있는 로직을 캡슐화하여 재사용할 수 있게 해줍니다. + - 예를 들어, `useForm`이라는 사용자 정의 훅(Custom Hook)을 만들어 폼 처리 로직을 여러 컴포넌트에서 사용할 수 있습니다. + ```jsx + const [formData, handleInputChange] = useForm(initialValues); + ``` + +### 3.2.2 고차 컴포넌트(Higher Order Component;HOC) + +- 고차 컴포넌트(Higher Order Component;HOC)는 컴포넌트를 인자로 받아 새로운 컴포넌트를 반환하는 함수입니다. +- HOC는 주로 React의 클래스 컴포넌트에서 사용되었으며, 컴포넌트 간에 공유할 수 있는 로직을 추상화하는 데 사용됩니다. + - 예를 들어, `withUser`라는 HOC를 사용해 여러 컴포넌트에 사용자 데이터를 주입할 수 있습니다. +- 고차 컴포넌트 사용 시 주의 해야할 점 + - 고차 컴포넌트(Higher Order Component;HOC)는 `with`라는 접두사를 시작해야한다. + ```jsx + const EnhancedComponent = withUser(BaseComponent); + ``` + - 부수 효과를 최소화해야 한다. + - 여러개의 고차 컴포넌트로 컴포넌트를 감쌀 경우 복잡성이 커질 수 있다. +- `React.memo` + - `React.memo`는 React의 기능 중 하나로, 컴포넌트를 메모이제이션하는 데 사용됩니다. + - `React.memo`는 컴포넌트가 동일한 props로 렌더링될 때 이전에 렌더링한 결과를 재사용합니다. 이를 통해 불필요한 렌더링을 방지하고 애플리케이션의 성능을 향상시킬 수 있습니다. + - `React.memo`를 사용하려면 함수형 컴포넌트를 생성하고 `React.memo` 함수로 해당 컴포넌트를 감싸주면 됩니다. + - `React.memo`는 두 번째 매개변수로 이전 props와 다음 props를 비교하는 함수를 받을 수도 있습니다. 이 함수를 사용하면 어떤 prop이 변경되었을 때에만 컴포넌트가 다시 렌더링되도록 할 수 있습니다. + - `React.memo`는 주로 무거운 계산이나 비용이 많이 드는 작업을 수행하는 컴포넌트의 성능을 최적화하는 데 사용됩니다. 그러나 모든 컴포넌트에 `React.memo`를 사용하는 것은 권장되지 않습니다. 성능 향상이 미미하거나 컴포넌트 자체가 빈번하게 업데이트되는 경우에는 오히려 부작용이 발생할 수 있습니다. + +### 3.2.3 사용자 정의 훅과 고차 컴포넌트 중 무엇을 써야 할까? + +고차 컴포넌트는 주로 렌더링 로직이나 생명주기 메서드와 같은 컴포넌트의 구조적인 부분에 관여하는 반면, 사용자 정의 훅은 보다 세밀한 데이터 관리나 사이드 이펙트 로직 등에 더 적합할 수 있습니다. + +#### 사용자 정의 훅이 필요한 경우 + +- 리액트의 기본 훅(`useState`, `useEffect` 등)을 사용하여 간단하게 공통 로직을 분리할 수 있는 경우 +- 컴포넌트 내부에서의 영향을 최소화하고, 개발자가 원하는 방식으로 훅을 사용하고자 할 때 +- 컴포넌트 전반에 걸쳐 동일한 로직을 적용하거나, 특정한 상태 관리 패턴을 구현하고 싶을 때 + +#### 고차 컴포넌트를 사용해야 하는 경우 + +- 컴포넌트가 에러를 처리하고 에러 발생 시 대체 컴포넌트를 보여주어야 할 때 +- 컴포넌트의 렌더링 결과에 직접적인 영향을 주어야 하거나, 컴포넌트 트리에 추가적인 요소를 삽입해야 할 때 + +### 3.2.4 정리 + +
+ +## References + +- [리액트 공식문서, Introducing Hooks](https://legacy.reactjs.org/docs/hooks-intro.html) +- [리액트 공식문서, Hooks API Reference](https://legacy.reactjs.org/docs/hooks-reference.html) +- [useState](https://react.dev/reference/react/useState) +- [React 공식 문서, Rules of Hooks](https://legacy.reactjs.org/docs/hooks-rules.html) + - (한국어 버전) [React 공식 문서, Rules of Hooks](https://ko.legacy.reactjs.org/docs/hooks-rules.html) +- [React Dev, Rule of Hooks](https://react.dev/warnings/invalid-hook-call-warning) +- [React Dev, useState](https://react.dev/reference/react/useState) + +
+ +## Articles + +- [DEV, Why useEffect is running twice in react](https://dev.to/jahid6597/why-useeffect-is-running-twice-in-react-18c6#:~:text=In%20React%2C%20the%20useEffect%20hook,as%20%22dependencies%22)%20change) +- [Cleanup Function with useEffect in React: The Right Way to Clean Up After Yourself!](https://www.linkedin.com/pulse/cleanup-function-useeffect-react-right-way-clean-up-after-mushnik/) +- [HOC Vs Custom Hooks which is better for code sharing? Or simply write a function and import it and use it?](https://www.reddit.com/r/reactjs/comments/rzmowa/hoc_vs_custom_hooks/) +- [What is the difference between custom Hooks and HOC?](https://www.turing.com/blog/custom-react-js-hooks-how-to-use/#:~:text=HOCs%20can%20introduce%20complexity%20with,for%20inheritance%20or%20prop%20drilling) +- [How do you name custom Hooks?](https://www.linkedin.com/pulse/understanding-custom-hooks-reactjs-simplifying-logic-hossein-safari#:~:text=The%20naming%20convention%20for%20custom,the%20necessary%20rules%20and%20optimizations) +- [Why use custom Hooks?](https://react.dev/learn/reusing-logic-with-custom-hooks#:~:text=Custom%20Hooks%20let%20you%20share%20stateful%20logic%2C%20not%20state%20itself&text=They%20happened%20to%20have%20the,whether%20the%20network%20is%20on).&text=There's%20some%20repetitive%20logic%20for,state%20(%20firstName%20and%20lastName%20)) +- [React.memo 현명하게 사용하기](https://ui.toast.com/weekly-pick/ko_20190731#reactmemo-%ED%98%84%EB%AA%85%ED%95%98%EA%B2%8C-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0) +- [A guide to memoization using React.memo](https://github.com/RicardoMorato/React.memo) +- [Higher-Order Components In React](https://www.smashingmagazine.com/2020/06/higher-order-components-react/) +- [HOC Pattern](https://www.patterns.dev/react/hoc-pattern/) +- [Cleaning up with useEffect function](https://www.zipy.ai/blog/understanding-react-useeffect-cleanup-function#:~:text=This%20cleanup%20function%20is%20used,side%20effects%20one%20by%20one.) diff --git a/chapter03/info.md b/chapter03/info.md index f920e2f..65439be 100644 --- a/chapter03/info.md +++ b/chapter03/info.md @@ -4,25 +4,24 @@
-- [03장: 리액트 훅 깊게 살펴보기](#03장-리액트-훅-깊게-살펴보기) - - [3.1 리액트 훅 깊게 살펴보기](#31-리액트-훅-깊게-살펴보기) - - [3.1.1 useState](#311-usestate) - - [3.1.2 useEffect](#312-useeffect) - - [3.1.3 useMemo](#313-usememo) - - [3.1.4 useCallback](#314-usecallback) - - [3.1.5 useRef](#315-useref) - - [3.1.6 useContext](#316-usecontext) - - [3.1.7 useReducer](#317-usereducer) - - [3.1.8 useImperativeHandle](#318-useimperativehandle) - - [3.1.9 useLayoutEffect](#319-uselayouteffect) - - [3.1.10 useDebugValue](#3110-usedebugvalue) - - [3.1.11 훅의 규칙](#3111-훅의-규칙) - - [3.1.12 정리](#3112-정리) - - [3.2 사용자 정의 훅과 고차 컴포넌트 중 무엇을 써야 할까?](#32-사용자-정의-훅과-고차-컴포넌트-중-무엇을-써야-할까) - - [3.2.1 사용자 정의 훅](#321-사용자-정의-훅) - - [3.2.2 고차 컴포넌트](#322-고차-컴포넌트) - - [3.2.3 사용자 정의 훅과 고차 컴포넌트 중 무엇을 써야 할까?](#323-사용자-정의-훅과-고차-컴포넌트-중-무엇을-써야-할까) - - [3.2.4 정리](#324-정리) +- [3.1 리액트의 모든 훅 파헤치기](#31-리액트의-모든-훅-파헤치기) + - [3.1.1 useState](#311-usestate) + - [3.1.2 useEffect](#312-useeffect) + - [3.1.3 useMemo](#313-usememo) + - [3.1.4 useCallback](#314-usecallback) + - [3.1.5 useRef](#315-useref) + - [3.1.6 useContext](#316-usecontext) + - [3.1.7 useReducer](#317-usereducer) + - [3.1.8 useImperativeHandle](#318-useimperativehandle) + - [3.1.9 useLayoutEffect](#319-uselayouteffect) + - [3.1.10 useDebugValue](#3110-usedebugvalue) + - [3.1.11 훅의 규칙](#3111-훅의-규칙) + - [3.1.12 정리](#3112-정리) +- [3.2 사용자 정의 훅과 고차 컴포넌트 중 무엇을 써야 할까?](#32-사용자-정의-훅과-고차-컴포넌트-중-무엇을-써야-할까) + - [3.2.1 사용자 정의 훅](#321-사용자-정의-훅) + - [3.2.2 고차 컴포넌트](#322-고차-컴포넌트) + - [3.2.3 사용자 정의 훅과 고차 컴포넌트 중 무엇을 써야 할까?](#323-사용자-정의-훅과-고차-컴포넌트-중-무엇을-써야-할까) + - [3.2.4 정리](#324-정리)
@@ -62,4 +61,4 @@ ### 3.2.3 사용자 정의 훅과 고차 컴포넌트 중 무엇을 써야 할까? -### 3.2.4 정리 +### 3.2.4 정리 \ No newline at end of file diff --git a/examples/chapter03/usState.jsx b/examples/chapter03/usState.jsx new file mode 100644 index 0000000..e69de29 diff --git a/examples/chapter03/useEffect-1.jsx b/examples/chapter03/useEffect-1.jsx new file mode 100644 index 0000000..f24283c --- /dev/null +++ b/examples/chapter03/useEffect-1.jsx @@ -0,0 +1,8 @@ +// useEffect의 일반적인 형태 +function Component() { + // ... + useEffect(() => { + // do sth + }, [props, state]) + // ... +} \ No newline at end of file diff --git a/examples/chapter03/useEffect-10.tsx b/examples/chapter03/useEffect-10.tsx new file mode 100644 index 0000000..1ef5890 --- /dev/null +++ b/examples/chapter03/useEffect-10.tsx @@ -0,0 +1,9 @@ +function Component({ + log +}: { + log: string +}){ + useEffect(() => { + logging(log) + }, []) // eslint-disable-line react-hooks/exhaustive-deps +} \ No newline at end of file diff --git a/examples/chapter03/useEffect-11.jsx b/examples/chapter03/useEffect-11.jsx new file mode 100644 index 0000000..60a40f6 --- /dev/null +++ b/examples/chapter03/useEffect-11.jsx @@ -0,0 +1,3 @@ +useEffect(() => { + logging(user.id) +}, [user.id]) \ No newline at end of file diff --git a/examples/chapter03/useEffect-12.jsx b/examples/chapter03/useEffect-12.jsx new file mode 100644 index 0000000..5ef7895 --- /dev/null +++ b/examples/chapter03/useEffect-12.jsx @@ -0,0 +1,5 @@ +useEffect(() => { + function logActiveUser() { + logging(user.id) + } +}, [user.id]) \ No newline at end of file diff --git a/examples/chapter03/useEffect-13.tsx b/examples/chapter03/useEffect-13.tsx new file mode 100644 index 0000000..e69de29 diff --git a/examples/chapter03/useEffect-14.jsx b/examples/chapter03/useEffect-14.jsx new file mode 100644 index 0000000..e69de29 diff --git a/examples/chapter03/useEffect-15.jsx b/examples/chapter03/useEffect-15.jsx new file mode 100644 index 0000000..e69de29 diff --git a/examples/chapter03/useEffect-2.jsx b/examples/chapter03/useEffect-2.jsx new file mode 100644 index 0000000..a087691 --- /dev/null +++ b/examples/chapter03/useEffect-2.jsx @@ -0,0 +1,18 @@ +/** + * + * @returns 버튼 클릭시 counter가 1씩 중가 + */ +function Component() { + const [counter, setCounter] = useState(0); + + function handleClick() { + setCounter((prev) => prev + 1); + } + + return ( + <> +

{counter}

+ + + ) +} \ No newline at end of file diff --git a/examples/chapter03/useEffect-3.jsx b/examples/chapter03/useEffect-3.jsx new file mode 100644 index 0000000..3fe9df9 --- /dev/null +++ b/examples/chapter03/useEffect-3.jsx @@ -0,0 +1,10 @@ +function Component() { + const counter = 1;; + // ... + return ( + <> +

{counter}

+ + + ) +} \ No newline at end of file diff --git a/examples/chapter03/useEffect-4.jsx b/examples/chapter03/useEffect-4.jsx new file mode 100644 index 0000000..82d0b32 --- /dev/null +++ b/examples/chapter03/useEffect-4.jsx @@ -0,0 +1,14 @@ +function Component() { + const counter = 1;; + + useEffect(() => { + console.log(counter) + }) + + return ( + <> +

{counter}

+ + + ) +} \ No newline at end of file diff --git a/examples/chapter03/useEffect-5.jsx b/examples/chapter03/useEffect-5.jsx new file mode 100644 index 0000000..4816dd1 --- /dev/null +++ b/examples/chapter03/useEffect-5.jsx @@ -0,0 +1,39 @@ +import { useState, useEffect } from 'react'; + +export default function App() { + const [counter, setCounter] = useState(0); + + function handleClick() { + setCounter((prev) => prev + 1) + } + + useEffect(() => { + function addMouseEvent() { + console.log(counter); + } + + window.addEventListener('click', addMouseEvent) + + // clean-up + return () => { + console.log('clean-up 함수 실행', counter); + window.removeEventListener('click', addMouseEvent) + } + }, [counter]) + + return ( + <> +

{counter}

+ + + ) + +} + +/** + * @return clean-up 함수를 사용하여 이벤트 리스너를 제거 + * clean-up 함수 실행: 0 + * 1 + * clean-up 함수 실행: 1 + * 2 + */ \ No newline at end of file diff --git a/examples/chapter03/useEffect-6.jsx b/examples/chapter03/useEffect-6.jsx new file mode 100644 index 0000000..46f8087 --- /dev/null +++ b/examples/chapter03/useEffect-6.jsx @@ -0,0 +1,30 @@ +// 최초 실행 +useEffect(() => { + function addMouseEvent() { + console.log(1); + } + + window.addEventListener('click', addMouseEvent) + + // clean-up 함수 + return () => { + console.log('clean-up 함수 실행', 1); + window.removeEventListener('click', addMouseEvent) + } +}, [counter]) + +// 이후 실행 +useEffect(() => { + function addMouseEvent() { + console.log(2); + } + + window.addEventListener('click', addMouseEvent) + + // clean-up 함수 + return () => { + console.log('clean-up 함수 실행', 2); + window.removeEventListener('click', addMouseEvent) + } +}, [counter]) + diff --git a/examples/chapter03/useEffect-7.jsx b/examples/chapter03/useEffect-7.jsx new file mode 100644 index 0000000..7dfd450 --- /dev/null +++ b/examples/chapter03/useEffect-7.jsx @@ -0,0 +1,11 @@ +// 1 +function Component() { + console.log('렌더링 됨'); +} + +// 2 +function Component() { + useEffect(() => { + console.log('렌더링 됨'); + }) +} diff --git a/examples/chapter03/useEffect-8.jsx b/examples/chapter03/useEffect-8.jsx new file mode 100644 index 0000000..8727867 --- /dev/null +++ b/examples/chapter03/useEffect-8.jsx @@ -0,0 +1,33 @@ +const MyReact = (function () { + const global = {} + let index = 0 + + function useEffect(callback, dependencies) { + const hooks = global.hooks + + // 이전 훅 정보가 있는지 확인한다. + let previousDependencies = hooks[index] + + // 변경되었는지 확인 + // 이전 값이 있다면 이전 값을 얕은 비교로 비교해 변경이 일어났는지 확인한다. + // 이전 값이 없다면 최초 실행이므로 변경이 일어난 것으로 간주해 실행을 유도한다. + let isDependenciesChanged = previousDependencies ? dependencies.some( + (value, index) => !Object.is(value, previousDependencies[idx]) + ) : true + + // 변경이 일어났다면 첫번째 인수인 콜백 함수를 실행 + if (isDependenciesChanged) { + callback() + } + + // 현재 의존성을 훅에 다시 저장 + hooks[index] = dependencies + + // 다음 훅이 일어날 때를 대비하기 위해 index를 추가 + index++ + } + + return { + useEffect + } +})() \ No newline at end of file diff --git a/examples/chapter03/useEffect-9.jsx b/examples/chapter03/useEffect-9.jsx new file mode 100644 index 0000000..a646e8f --- /dev/null +++ b/examples/chapter03/useEffect-9.jsx @@ -0,0 +1,3 @@ +useEffect(() => { + console.log(props); +}, []) // eslint-disable-line react-hooks/exhaustive-deps