diff --git "a/week-03/\353\257\274\355\230\201.md" "b/week-03/\353\257\274\355\230\201.md" new file mode 100644 index 0000000..ea89adf --- /dev/null +++ "b/week-03/\353\257\274\355\230\201.md" @@ -0,0 +1,1022 @@ +# useState + +## 함수 내부에서 자체적으로 변수를 사용해 값을 변경한다면? + +```ts +function Component() { + let state = 'hello'; + + function handleButtonClick() { + state = 'hi'; + } + + return ( + <> +

{state}

+ + + ); +} +``` + +위 코드에서 `handleButtonClick` 함수를 클릭하여도 화면 상의 `state`는 변경되지 않는다. +**화면 상의 값이 변경되려면 리렌더링이 일어나야 하는데**, 위 코드만으로는 리렌더링이 일어나지 않는다. + +```ts +const [, trigger] = useState(); +let state = 'hello'; + +function handleButtonClick() { + state = 'hi'; + trigger(); +} +``` + +위 코드에서는 `useState`에 의하여 리렌더링이 일어난다. +하지만, 리렌더링이 일어나면서 `state` 값도 초기화되기 때문에, 결국 `state`는 다시 `'hello'`로 초기화된다. + +--- + +## 게으른 초기화 + +`useState`의 초기값으로 일반 값을 전달할 수도 있지만, 콜백 함수를 전달할 수도 있다. + +```ts +const [state, setState] = useState(() => { + return 'hello'; +}); +``` + +이 방식을 게으른 초기화라고 한다. + +> `useState`의 초기값에 콜백 함수를 통해 반환값을 전달하면, 리렌더링 되어도 `state` 값이 유지된다. +즉, 리렌더링이 발생해도 콜백 함수는 다시 실행되지 않으며, `React`가 저장해둔 `state` 변경되지 않는다. + +--- + +### 어떤 경우에 게으른 초기화를 하면 좋을까? + +게으른 초기화는 초기값을 계산하는 비용이 큰 경우 유용하다. +예를 들어, `localStorage`에서 값을 가져오거나, `map`처럼 배열에 대한 접근, `JSON.parse`를 수행하는 경우에 사용할 수 있다. + +```ts +const [value, setValue] = useState(() => { + return localStorage.getItem('value') ?? 'default'; +}); +``` + +이렇게 작성하면 `localStorage`에서 값을 가져오는 작업은 초기 렌더링 때만 실행되고, 이후 리렌더링에서는 기존 `state` 값이 유지된다. + +--- + +# useEffect + +## useEffect란? + +`useEffect`는 컴포넌트가 렌더링된 이후 특정 작업, 즉 **부수 효과(Side Effect)**를 처리하기 위해 사용하는 React Hook이다. + +대표적인 부수 효과에는 다음과 같은 작업이 있다. + +- API 요청 +- 이벤트 리스너 등록 +- 타이머 실행 +- 외부 라이브러리 연동 +- DOM 직접 조작 + +```tsx +useEffect(() => { + // 첫 번째 인수: 실행할 콜백 함수 + console.log('컴포넌트가 처음 렌더링될 때 실행된다.'); +}, []); // 두 번째 인수: 의존성 배열 +``` + +`useEffect`는 두 개의 인수를 받는다. + +1. 첫 번째 인수: 실행할 콜백 함수 +2. 두 번째 인수: 의존성 배열 + +의존성 배열에 빈 배열 `[]`을 넣으면 컴포넌트가 **처음 마운트될 때만** 실행된다. + +--- + +## useEffect의 마운트와 언마운트 + +### 마운트(mount)란? + +> 💡 컴포넌트가 처음 생성되고, 실제 브라우저 DOM에 삽입되어 화면에 나타나는 과정 + +즉, 컴포넌트가 화면에 처음 등장하는 시점을 의미한다. + +### 언마운트(unmount)란? + +> 💡 컴포넌트가 화면에서 제거되고, DOM에서도 사라지는 과정 + +즉, 컴포넌트가 더 이상 화면에 표시되지 않는 시점을 의미한다. + +```tsx +useEffect(() => { + // 컴포넌트가 마운트될 때 실행된다. + + return () => { + // 컴포넌트가 언마운트될 때 실행된다. + }; +}, []); // 의존성 배열 +``` + +위 코드에서 `useEffect`의 콜백 함수는 컴포넌트가 마운트될 때 실행된다. + +그리고 `return` 안의 함수는 컴포넌트가 언마운트될 때 실행된다. 이 함수를 **클린업 함수**라고 한다. + +--- + +## 클린업 함수 + +클린업 함수는 `useEffect` 내부에서 `return`하는 함수다. + +> 💡 컴포넌트가 언마운트될 때, 또는 effect가 다시 실행되기 전에 이전 effect를 정리하기 위해 사용된다. + +```tsx +useEffect(() => { + const timerId = setInterval(() => { + console.log('1초마다 실행된다.'); + }, 1000); + + // 클린업 함수 + return () => { + clearInterval(timerId); + console.log('Cleanup: Timer cleared'); + }; +}, []); +``` + +위 코드는 컴포넌트가 마운트되면 `setInterval`을 실행한다. + +그리고 컴포넌트가 언마운트되면 클린업 함수에서 `clearInterval`을 실행하여 타이머를 정리한다. + +빈 의존성 배열 `[]`을 전달했기 때문에 effect는 컴포넌트가 마운트될 때 실행되고, 클린업 함수는 언마운트될 때 실행된다. + +--- + +## 클린업 함수를 사용하지 않으면 발생할 수 있는 문제 + +`useEffect` 안에서 이벤트 리스너, 타이머, 구독과 같은 작업을 실행했다면 컴포넌트가 사라질 때 해당 작업을 정리해야 한다. + +클린업 함수를 사용하지 않으면 컴포넌트가 언마운트된 이후에도 이전에 등록한 작업이 계속 남아 있을 수 있다. + +### 1. 메모리 누수 + +컴포넌트가 화면에서 사라졌는데도 이벤트 리스너나 타이머가 계속 실행되면 불필요한 메모리를 계속 사용하게 된다. + +예를 들어 `setInterval`을 사용하고 클린업 함수에서 제거하지 않으면, 컴포넌트가 사라진 뒤에도 타이머가 계속 동작할 수 있다. + +```tsx +useEffect(() => { + const timerId = setInterval(() => { + console.log('타이머 실행'); + }, 1000); + + return () => { + clearInterval(timerId); + }; +}, []); +``` + +### 2. 예상치 못한 동작 + +클린업을 하지 않으면 이전에 등록된 이벤트나 타이머가 중복으로 실행될 수 있다. + +예를 들어 버튼 클릭 이벤트를 등록했는데 컴포넌트가 다시 렌더링될 때마다 이벤트가 계속 추가된다면, 한 번 클릭했을 뿐인데 같은 함수가 여러 번 실행될 수 있다. + +```tsx +useEffect(() => { + const handleClick = () => { + console.log('클릭됨'); + }; + + window.addEventListener('click', handleClick); + + return () => { + window.removeEventListener('click', handleClick); + }; +}, []); +``` + +이처럼 `useEffect`에서 외부에 영향을 주는 작업을 했다면 클린업 함수를 통해 정리하는 것이 좋다. + +--- + +## 의존성 배열이 없는 useEffect + +```tsx +useEffect(() => { + console.log('렌더링될 때마다 실행된다.'); +}); +``` + +의존성 배열을 전달하지 않은 `useEffect`는 컴포넌트가 렌더링될 때마다 실행된다. + +그렇다면 다음과 같은 의문이 생길 수 있다. + +> ❓ 의존성 배열이 없는 `useEffect`가 매 렌더링마다 실행된다면, 굳이 `useEffect`를 사용할 필요가 있을까? + +결론부터 말하면, 필요하다. + +`useEffect`는 컴포넌트의 렌더링이 완료된 이후에 실행된다. 즉, 화면을 그리는 작업이 끝난 뒤 부수 효과를 처리한다. + +반면 컴포넌트 함수 내부에 코드를 직접 작성하면, 해당 코드는 렌더링 과정 중에 실행된다. + +```tsx +function Example() { + console.log('렌더링 중 실행된다.'); + + return
Example
; +} +``` + +위 코드는 컴포넌트가 렌더링되는 도중 실행된다. + +만약 이 위치에서 무거운 작업을 실행하면 컴포넌트가 JSX를 반환하는 시점이 늦어질 수 있다. 결과적으로 화면 렌더링을 방해하고 성능에 악영향을 줄 수 있다. + +반면 `useEffect` 안에 작성한 코드는 렌더링이 끝난 이후 실행된다. + +```tsx +function Example() { + useEffect(() => { + console.log('렌더링 이후 실행된다.'); + }); + + return
Example
; +} +``` + +따라서 렌더링 결과에 직접 영향을 주지 않는 작업은 컴포넌트 내부에 바로 작성하기보다 `useEffect` 안에서 처리하는 것이 적절하다. + +--- + +```tsx +useEffect(() => { + // 매 렌더링 후 실행 +}); + +useEffect(() => { + // 처음 마운트될 때만 실행 +}, []); + +useEffect(() => { + // count가 변경될 때 실행 +}, [count]); +``` + +--- + +# useMemo + +## useMemo란? + +`useMemo`는 계산 결과를 메모이제이션하기 위해 사용하는 React Hook이다. + +> 💡 메모이제이션이란, 이전에 계산한 값을 저장해두고 같은 조건에서는 다시 계산하지 않고 저장된 값을 재사용하는 기법이다. + +React 컴포넌트는 상태(state)나 props가 변경되면 다시 렌더링된다. + +이때 컴포넌트 내부의 변수나 함수도 다시 실행된다. 만약 컴포넌트 안에서 무거운 계산을 하고 있다면, 렌더링될 때마다 같은 계산이 반복되어 성능에 영향을 줄 수 있다. + +`useMemo`는 이런 불필요한 계산을 줄이기 위해 사용한다. + +```tsx +const memoizedValue = useMemo(() => { + // 첫 번째 인수: 값을 계산하는 콜백 함수 + return expensiveCalculation(); +}, []); // 두 번째 인수: 의존성 배열 +``` + +`useMemo`는 두 개의 인수를 받는다. + +1. 첫 번째 인수: 값을 계산해서 반환하는 콜백 함수 +2. 두 번째 인수: 의존성 배열 + +의존성 배열의 값이 변경되지 않으면, 이전에 계산한 값을 재사용한다. + +--- + +## useMemo 기본 예시 + +```tsx +import { useMemo, useState } from 'react'; + +function Example() { + const [count, setCount] = useState(0); + const [text, setText] = useState(''); + + const doubledCount = useMemo(() => { + console.log('count 계산 실행'); + return count * 2; + }, [count]); + + return ( +
+

count: {count}

+

doubledCount: {doubledCount}

+ + + + setText(e.target.value)} + placeholder="텍스트 입력" + /> +
+ ); +} +``` + +위 코드에서 `doubledCount`는 `count` 값을 기준으로 계산된다. + +```tsx +const doubledCount = useMemo(() => { + console.log('count 계산 실행'); + return count * 2; +}, [count]); +``` + +의존성 배열에 `[count]`가 들어 있기 때문에 `count`가 변경될 때만 다시 계산된다. + +반대로 `text` 값이 변경되어 컴포넌트가 다시 렌더링되더라도, `count`가 바뀌지 않았다면 `doubledCount`는 다시 계산되지 않고 이전 값을 재사용한다. + +## 객체와 배열에서 useMemo가 필요한 경우 + +JavaScript에서 객체와 배열은 값이 같아 보여도 새로 생성되면 다른 참조값을 가진다. + +```tsx +const user = { + name: 'Kim', + age: 20, +}; +``` + +위 코드는 컴포넌트가 렌더링될 때마다 새로운 객체를 만든다. + +객체의 내용이 같더라도 메모리 주소가 다르기 때문에 React는 다른 값으로 판단할 수 있다. + +```tsx +import { useMemo } from 'react'; + +function Parent() { + const user = useMemo(() => { + return { + name: 'Kim', + age: 20, + }; + }, []); + + return ; +} +``` + +위 코드처럼 `useMemo`를 사용하면 `user` 객체는 처음 렌더링될 때만 생성된다. + +이후 다시 렌더링되어도 같은 객체 참조값을 유지한다. + +--- + +## useMemo와 React.memo의 차이 + +[React.memo에 대해](https://github.com/DeepDive-FE/DeepDive-React/blob/%EB%AF%BC%ED%98%81/week-03/week-02/%EA%B9%80%EB%AF%BC%ED%98%81.md#reactmemo) + +`useMemo`와 `React.memo`는 모두 불필요한 작업을 줄이기 위해 사용한다. + +하지만 메모이제이션하는 대상이 다르다. + +| 구분 | useMemo | React.memo | +| --- | --- | --- | +| 종류 | Hook | 고차 컴포넌트 | +| 메모이제이션 대상 | 계산된 값 | 컴포넌트 | +| 사용 위치 | 컴포넌트 내부 | 컴포넌트 선언부 | +| 목적 | 불필요한 계산 방지 | 불필요한 컴포넌트 렌더링 방지 | +| 비교 기준 | 의존성 배열 | props | +| 대표 사용 예시 | 필터링, 정렬, 합계 계산 결과 저장 | props가 같을 때 자식 컴포넌트 렌더링 방지 | + +--- + +### useMemo로도 컴포넌트를 메모이제이션할 수 있지 않나? + + +```tsx +const memoizedChild = useMemo(() => { + return ; +}, [name]); +``` + +이 방식은 가능하지만, 컴포넌트를 최적화한다는 의도가 `React.memo`보다 덜 직관적일 수 있다. + +정확히 말하면 컴포넌트 자체를 메모이제이션한다기보다는, **컴포넌트가 반환된 JSX 결과값을 메모이제이션하는 것**에 가깝다. + +반면 `React.memo`는 **컴포넌트 자체를 메모이제이션**한다. + +--- + +# useCallback + +`useCallback`은 **함수를 메모이제이션**하기 위해 사용하는 React Hook이다. + +즉, 컴포넌트가 다시 렌더링되더라도 특정 함수를 매번 새로 만들지 않고, 이전에 만들어둔 함수를 재사용할 수 있게 해준다. + +`useMemo`가 **계산된 값**을 기억한다면, `useCallback`은 **함수 자체**를 기억한다. + +```tsx +const handleClick = useCallback(() => { + console.log('버튼 클릭'); +}, []); +``` + +`useCallback`은 두 개의 인수를 받는다. + +1. 첫 번째 인수: 기억할 콜백 함수 +2. 두 번째 인수: 의존성 배열 + +```tsx +const handleClick = useCallback(() => { + // 첫 번째 인수: 기억할 콜백 함수 + console.log('버튼 클릭'); +}, []); // 두 번째 인수: 의존성 배열 +``` + +의존성 배열의 값이 변경되지 않으면 이전에 생성한 함수를 그대로 재사용한다. + +--- + +## useCallback을 사용하는 이유 + +부모 컴포넌트가 리렌더링되면 부모 내부에 선언된 함수도 새로 생성된다. + +이 함수가 자식 컴포넌트의 props로 전달되면, 자식 컴포넌트는 props가 변경되었다고 판단할 수 있다. + +이때 `useCallback`을 사용하면 함수의 참조값을 유지할 수 있다. + +--- + +# useRef + +`useRef`는 특정 DOM 요소에 직접 접근하거나, 컴포넌트가 다시 렌더링되더라도 변하지 않는 값을 기억하고 싶을 때 사용하는 React Hook이다. + +`useState`와 비슷하게 값을 저장할 수 있지만, `useRef`는 값이 변경되어도 컴포넌트가 리렌더링되지 않는다. + +```tsx +const countRef = useRef(0); + +countRef.current += 1; +``` + +`useRef`는 객체를 반환하고, 실제 값은 `.current`에 저장된다. + +```tsx +const ref = useRef(initialValue); +``` + +- `ref.current`: 저장된 값 +- `initialValue`: 처음 저장할 값 + +--- + +## useRef를 사용하는 이유 + +컴포넌트가 리렌더링되어도 유지되어야 하지만, 값이 바뀐다고 화면을 다시 그릴 필요는 없는 경우에 사용한다. + +대표적으로 다음과 같은 경우에 사용한다. + +- DOM 요소에 직접 접근할 때 +- `setTimeout`, `setInterval`, `requestAnimationFrame`의 ID를 저장할 때 +- 이전 값이나 최신 상태 값을 저장할 때 +- 값은 유지해야 하지만 리렌더링은 발생시키고 싶지 않을 때 + +--- + +## 기본 예시 + +```tsx +import { useRef } from 'react'; + +function Example() { + const inputRef = useRef(null); + + const focusInput = () => { + inputRef.current?.focus(); + }; + + return ( +
+ + +
+ ); +} +``` + +위 코드에서 `inputRef`는 `input` DOM 요소를 참조한다. + +버튼을 클릭하면 `inputRef.current`를 통해 실제 input 요소에 접근하고, `focus()`를 실행할 수 있다. + +--- + +## useState와 useRef의 차이 + +`useState`와 `useRef`는 모두 값을 저장할 수 있다. + +하지만 값이 변경되었을 때 동작 방식이 다르다. + +| 구분 | useState | useRef | +| --- | --- | --- | +| 값 저장 | 가능 | 가능 | +| 값 변경 시 리렌더링 | 발생함 | 발생하지 않음 | +| 값 접근 방식 | state 변수 | `ref.current` | +| 주 사용 목적 | 화면에 반영되는 상태 관리 | 렌더링과 무관한 값 저장 | + +```tsx +const [count, setCount] = useState(0); +const countRef = useRef(0); +``` + +`count`는 값이 바뀌면 화면을 다시 렌더링한다. + +반면 `countRef.current`는 값이 바뀌어도 화면을 다시 렌더링하지 않는다. + +--- + +# useContext + +`useContext`는 `React`에서 컴포넌트 간에 데이터를 전역적으로 공유할 수 있게 해주는 Hook이다. + +## Context란? + +`Context`는 컴포넌트 트리 전체에 값을 공유할 수 있게 해주는 `React`의 기능이다. + +일반적으로 `React`에서는 부모 컴포넌트가 자식 컴포넌트에게 데이터를 전달할 때 `props`를 사용한다. + +하지만 여러 단계 아래에 있는 컴포넌트까지 같은 값을 전달해야 한다면, 중간 컴포넌트들이 직접 사용하지 않는 `props`를 계속 넘겨줘야 한다. + +```tsx + + + + + + + +``` + +이처럼 props를 여러 단계로 전달해야 하는 문제를 **props 내려주기(props drilling)**이라고 한다. + +`Context`를 사용하면 `props`를 여러 단계 전달하지 않아도, 필요한 컴포넌트에서 값을 바로 사용할 수 있다. + +```tsx +const value = useContext(MyContext); +``` + +`useContext`는 `Context` 객체를 인수로 받고, 해당 `Context`에서 제공하는 값을 반환한다. + +```tsx +const user = useContext(UserContext); +``` + +- `UserContext`: 공유할 값을 담고 있는 Context +- `user`: Context에서 꺼내온 값 + +--- + +## 기본 예시 + +```tsx +import { createContext, useContext } from 'react'; + +const UserContext = createContext<{ name: string } | null>(null); + +function App() { + const user = { name: 'Kim' }; + + return ( + + + + ); +} + +function Profile() { + const user = useContext(UserContext); + + return
이름: {user?.name}
; +} +``` + +위 코드에서 `UserContext.Provider`는 하위 컴포넌트들에게 `user` 값을 제공한다. + +`Profile` 컴포넌트는 props로 `user`를 받지 않아도 `useContext(UserContext)`를 통해 값을 가져올 수 있다. + +--- + +## Provider란? + +`Provider`는 `Context` 값을 하위 컴포넌트에게 전달하는 역할을 한다. + +```tsx + + + +``` + +`Provider`의 `value`에 넣은 값은 그 안에 포함된 모든 하위 컴포넌트에서 사용할 수 있다. + +```tsx +const user = useContext(UserContext); +``` + +이렇게 `useContext`를 사용하면 가장 가까운 상위 `Provider`의 값을 가져온다. + +--- + +## useContext 사용 시 주의할 점 + +`useContext`를 사용하면 `props` 전달을 줄일 수 있지만, 모든 상태를 `Context`로 관리하는 것은 좋지 않다. + +--- + +# useCallback + +`useCallback`은 **함수를 메모이제이션**하기 위해 사용하는 React Hook이다. + +즉, 컴포넌트가 다시 렌더링되더라도 특정 함수를 매번 새로 만들지 않고, 이전에 만들어둔 함수를 재사용할 수 있게 해준다. + +`useMemo`가 **계산된 값**을 기억한다면, `useCallback`은 **함수 자체**를 기억한다. + +```tsx +const handleClick = useCallback(() => { + console.log('버튼 클릭'); +}, []); +``` + +`useCallback`은 두 개의 인수를 받는다. + +1. 첫 번째 인수: 기억할 콜백 함수 +2. 두 번째 인수: 의존성 배열 + +```tsx +const handleClick = useCallback(() => { + // 첫 번째 인수: 기억할 콜백 함수 + console.log('버튼 클릭'); +}, []); // 두 번째 인수: 의존성 배열 +``` + +의존성 배열의 값이 변경되지 않으면 이전에 생성한 함수를 그대로 재사용한다. + +--- + +## useCallback을 사용하는 이유 + +부모 컴포넌트가 리렌더링되면 부모 내부에 선언된 함수도 새로 생성된다. + +이 함수가 자식 컴포넌트의 props로 전달되면, 자식 컴포넌트는 props가 변경되었다고 판단할 수 있다. + +이때 `useCallback`을 사용하면 함수의 참조값을 유지할 수 있다. + +다만 `useCallback`만으로 자식 컴포넌트의 리렌더링을 막는 것은 아니다. +자식 컴포넌트의 불필요한 리렌더링을 막고 싶다면 보통 `React.memo`와 함께 사용한다. +좋아 useReduecer도 써줘 +몇 초 동안 생각함 + +아래 그대로 .md 파일에 복사해서 사용하면 된다. + +# useReducer + +`useReducer`는 여러 상태를 하나의 규칙에 따라 관리할 때 사용하는 Hook이다. + +`useState`처럼 상태를 관리할 수 있지만, 상태 변경 로직을 컴포넌트 밖으로 분리할 수 있다는 특징이 있다. + +상태가 단순할 때는 `useState`를 사용해도 충분하지만, 상태 변경 방식이 복잡하거나 여러 조건에 따라 상태가 바뀐다면 `useReducer`를 사용할 수 있다. + +--- + +## useReducer 기본 형태 + +```tsx +const [state, dispatch] = useReducer(reducer, initialState, init); +``` + +`useReducer`는 `useState`와 동일하게 길이가 2인 배열이다. + +1. `state`: 현재 상태 +2. `dispatch`: 상태 변경을 요청하는 함수 + +그리고 `useReducer`는 보통 2 ~ 3개의 인수를 받는다. + +1. `reducer`: 상태 변경 로직을 담은 `reducer` 함수 +2. `initialState`: 초기 상태값 +3. `init` (필수값 X): 초기값을 지연해서 생성시킬 때 사용하는 함수. useState와 동일하게 게으른 초기화가 일어나며, `initialState`를 인수로 `init` 함수가 실행된다. + +--- + +### reducer 함수란? + +`reducer` 함수는 현재 상태와 action을 받아서 새로운 상태를 반환하는 함수다. + +```tsx +function reducer(state, action) { + switch (action.type) { + case 'INCREASE': + return state + 1; + + case 'DECREASE': + return state - 1; + + default: + return state; + } +} +``` + +여기서 `action`은 상태를 어떻게 변경할지 설명하는 객체다. + +```tsx +{ type: 'INCREASE' } +``` + +즉, `dispatch`로 action을 보내면 `reducer`가 그 `action`을 보고 상태를 변경한다. + +--- + +## useReducer 예시 + +```tsx +import { useReducer } from 'react'; + +function reducer(state: number, action: { type: 'INCREASE' | 'DECREASE' }) { + switch (action.type) { + case 'INCREASE': + return state + 1; + + case 'DECREASE': + return state - 1; + + default: + return state; + } +} + +function Counter() { + const [count, dispatch] = useReducer(reducer, 0); + + return ( +
+

현재 값: {count}

+ + + + +
+ ); +} +``` + +위 코드에서 `count`는 현재 상태이고, `dispatch`는 상태 변경을 요청하는 함수다. + +```tsx +dispatch({ type: 'INCREASE' }); +``` + +위 코드를 실행하면 `reducer` 함수에 현재 상태와 `action`이 전달된다. + +```tsx +reducer(count, { type: 'INCREASE' }); +``` + +그리고 `reducer`는 `action`의 `type`을 확인한 뒤 새로운 상태를 반환한다. + +--- + +## useReducer의 동작 흐름 + +```tsx + +``` + +위 버튼을 클릭하면 다음 순서로 동작한다. + +1. `dispatch` 함수가 실행된다. +2. `{ type: 'INCREASE' }` `action`이 `reducer` 함수로 전달된다. +3. `reducer` 함수가 현재 상태와 `action`을 비교한다. +4. `action type`에 맞는 새로운 상태를 반환한다. +5. `React`가 반환된 상태로 컴포넌트를 다시 렌더링한다. + +--- + +## 객체 상태를 관리하는 예시 + +`useReducer`는 여러 상태값을 하나의 객체로 관리할 때도 자주 사용한다. + +```tsx +import { useReducer } from 'react'; + +type State = { + count: number; + text: string; +}; + +type Action = + | { type: 'INCREASE' } + | { type: 'DECREASE' } + | { type: 'CHANGE_TEXT'; text: string }; + +const initialState: State = { + count: 0, + text: '', +}; + +function reducer(state: State, action: Action): State { + switch (action.type) { + case 'INCREASE': + return { + ...state, + count: state.count + 1, + }; + + case 'DECREASE': + return { + ...state, + count: state.count - 1, + }; + + case 'CHANGE_TEXT': + return { + ...state, + text: action.text, + }; + + default: + return state; + } +} + +function Example() { + const [state, dispatch] = useReducer(reducer, initialState); + + return ( +
+

count: {state.count}

+

text: {state.text}

+ + + + + + + dispatch({ + type: 'CHANGE_TEXT', + text: e.target.value, + }) + } + /> +
+ ); +} +``` + +위 코드에서는 `count`와 `text`를 하나의 `state` 객체로 관리한다. + +상태를 변경할 때는 직접 값을 수정하지 않고, `dispatch`를 통해 action을 전달한다. + +```tsx +dispatch({ type: 'CHANGE_TEXT', text: e.target.value }); +``` + +그러면 `reducer` 함수에서 `action type`에 맞게 새로운 상태를 반환한다. + +--- + +## useState와 useReducer의 차이 + +`useState`와 `useReducer`는 모두 상태를 관리하기 위한 Hook이다. + +다만 상태 변경 로직의 위치와 관리 방식에 차이가 있다. + +| 구분 | useState | useReducer | +| --- | --- | --- | +| 사용 목적 | 단순한 상태 관리 | 복잡한 상태 관리 | +| 상태 변경 방식 | setter 함수로 직접 변경 | dispatch로 action 전달 | +| 상태 변경 로직 | 컴포넌트 내부에 작성되는 경우가 많음 | reducer 함수에 분리 | +| 적합한 상황 | boolean, string, number 같은 단순 상태 | 여러 조건에 따라 바뀌는 객체 상태 | +| 예시 | `setCount(count + 1)` | `dispatch({ type: 'INCREASE' })` | + +--- + +## useReducer를 사용하는 상황 + +다음과 같은 경우에는 `useReducer`를 고려할 수 있다. + +- 상태값이 여러 개이고 서로 관련이 있는 경우 +- 상태 변경 로직이 복잡한 경우 +- 하나의 상태가 여러 `action`에 의해 변경되는 경우 +- 상태 변경 과정을 명확하게 관리하고 싶은 경우 +- 컴포넌트 내부에서 상태 변경 코드가 너무 길어지는 경우 + +--- + +# useLayoutEffect + +## 페인트(Paint)란? + +브라우저는 React 컴포넌트가 렌더링한 결과를 바로 화면에 보여주는 것이 아니라, 여러 단계를 거쳐 화면을 그린다. + +간단히 보면 다음과 같은 흐름이다. + +1. React가 컴포넌트를 렌더링한다. +2. 변경된 내용을 DOM에 반영한다. +3. 브라우저가 화면에 그릴 위치와 크기를 계산한다. +4. 브라우저가 실제 화면에 픽셀을 그린다. + +여기서 브라우저가 실제 화면에 내용을 그리는 과정을 **페인트(Paint)**라고 한다. + +> 💡 페인트란, 브라우저가 DOM과 스타일 계산 결과를 바탕으로 실제 화면에 UI를 그리는 과정이다. + +즉, 사용자가 화면에서 보는 결과가 만들어지는 단계라고 볼 수 있다. + +--- + +## useLayoutEffect란? + +`useLayoutEffect`는 React 컴포넌트가 렌더링되고 DOM에 반영된 직후, 브라우저가 화면을 페인트하기 전에 실행되는 Hook이다. + +```tsx +useLayoutEffect(() => { + console.log('브라우저가 화면을 그리기 전에 실행된다.'); +}, []); +``` + +`useLayoutEffect`는 형태상 `useEffect`와 거의 동일하다. + +```tsx +useLayoutEffect(() => { + // 첫 번째 인수: 실행할 콜백 함수 +}, []); // 두 번째 인수: 의존성 배열 +``` + +--- + +## useEffect와 useLayoutEffect의 실행 시점 차이 + +`useEffect`와 `useLayoutEffect`의 가장 큰 차이는 실행 시점이다. + +| 구분 | 실행 시점 | +| --- | --- | +| `useEffect` | 브라우저가 화면을 페인트한 후 실행 | +| `useLayoutEffect` | 브라우저가 화면을 페인트하기 전에 실행 | + +즉, `useEffect`는 화면이 먼저 그려진 후 실행되고, `useLayoutEffect`는 화면이 그려지기 전에 실행된다. + +--- + +### useLayoutEffect 실행 흐름 + +```tsx +useLayoutEffect(() => { + console.log('useLayoutEffect 실행'); +}, []); +``` + +`useLayoutEffect`는 다음과 같은 흐름으로 실행된다. + +1. 컴포넌트가 렌더링된다. +2. 변경된 내용이 DOM에 반영된다. +3. `useLayoutEffect`가 실행된다. +4. 브라우저가 화면을 페인트한다. +5. `useEffect`가 실행된다. + +즉, 사용자가 화면을 보기 전에 `useLayoutEffect` 안의 작업이 먼저 실행된다. + +--- + +## useLayoutEffect를 사용하는 이유 + +`useLayoutEffect`는 화면이 그려지기 전에 DOM을 읽거나 수정해야 할 때 사용한다. + +대표적으로 다음과 같은 경우에 사용할 수 있다. + +- DOM 요소의 크기나 위치를 측정해야 하는 경우 +- 측정한 값을 바탕으로 바로 스타일을 변경해야 하는 경우 +- 화면이 깜빡이는 현상을 막아야 하는 경우 +- 스크롤 위치를 조정해야 하는 경우 + +예를 들어 어떤 요소의 높이를 측정한 뒤, 그 높이에 따라 위치를 조정해야 한다고 가정해보자. + +이 작업을 `useEffect`에서 처리하면 브라우저가 먼저 화면을 그린 뒤 위치를 수정하기 때문에, 사용자가 순간적으로 잘못된 위치의 화면을 볼 수 있다. + +반면 `useLayoutEffect`는 화면이 그려지기 전에 실행되므로, 위치를 수정한 결과가 바로 화면에 나타난다.