From d3e2167e651e4bbd101207236c6521e468d7452a Mon Sep 17 00:00:00 2001 From: junye0l Date: Tue, 19 May 2026 23:49:50 +0900 Subject: [PATCH] =?UTF-8?q?docs(=EC=A4=80=EC=97=B4)=20:=203=EC=A3=BC?= =?UTF-8?q?=EC=B0=A8=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../\352\271\200\354\244\200\354\227\264.md" | 575 ++++++++++++++++++ 1 file changed, 575 insertions(+) create mode 100644 "week-03/\352\271\200\354\244\200\354\227\264.md" diff --git "a/week-03/\352\271\200\354\244\200\354\227\264.md" "b/week-03/\352\271\200\354\244\200\354\227\264.md" new file mode 100644 index 0000000..907b8f6 --- /dev/null +++ "b/week-03/\352\271\200\354\244\200\354\227\264.md" @@ -0,0 +1,575 @@ +

리액트 훅 깊게 살펴보기

+

useState

+ +- useState는 함수 컴포넌트 내부에서 상태를 정의하고, 관리하는 훅이다. + +- 매번 실행되는 함수 컴포넌트 환경에서 state 값을 유지하고 사용하기 위해서 리액트는 클로저를 활용하고 있다. +
+ +```tsx +// useState 기본 사용법 +import { useStaet } from "react" + +const [state, setState] = useState("초깃값"); // 초깃값이 없다면 undefined 할당 +``` + +

게으른 초기화

+ +- useState에 변수 대신 함수를 넘기는 것 + +- 리액트 컴포넌트가 렌더링될 때마다 상태 초기화 코드를 실행하는 대신, 초기화 함수는 첫 번째 렌더링 시에만 호출된다. + +- 무거운 연산이 요구될 때 (localStorage, sessionStorage에 대한 접근 + map, filter, find 등 배열에 대한 접근, 초깃값 계산) + +
+ +

useEffect

+ +- 콜백, 의존성 배열을 인수로 받는다. + +- 클래스 컴포넌트의 생명주기 메서드와 비슷한 작동을 구현할 수 있다. + +- 의존성 배열에 빈 배열을 두면 컴포넌트가 마운트될 때만 실행된다. + +- 클린업 함수는 컴포넌트가 언마운트될 때 실행된다. + +--- + +

useEffect는 어떻게 의존성 배열이 변경된 것을 알고 실행될까?

+ +- 컴포넌트가 처음 렌더링될 때, useEffect는 실행되고 의존성 배열(dependency array) 내의 값을 기록해둔다. + +- 이후 컴포넌트가 다시 렌더링되면 리액트는 새로운 렌더링에서 의존성 배열의 값과 이전 렌더링 시 기록해둔 값을 비교한다. + +- 리액트는 얕은 비교(참조 비교)를 사용하여 의존성 배열의 각 요소를 비교한다. + +- 의존성 배열의 값 중 하나라도 이전 값과 다른 경우, 리액트는 해당 useEffect를 다시 실행한다. + +- 부수 효과가 실행될 때마다 정리(clean-up) 함수가 실행된다(정리 함수가 반환된 경우). + +- 렌더링할 때마다 의존성에 있는 값을 보면서 이 의존성의 값이 이전과 다른 게 하나라도 있으면 부수효과를 실행하는 평범한 함수다. + +```tsx +import React, { useState, useEffect } from 'react'; + +function ExampleComponent() { + const [count, setCount] = useState(0); + const [text, setText] = useState(''); + + useEffect(() => { + console.log('부수 효과 실행: count 또는 text가 변경됨'); + + return () => { + console.log('정리 작업'); + }; + }, [count, text]); // count 또는 text가 변경될 때마다 실행됨 + + return ( +
+

카운트: {count}

+ + setText(e.target.value)} + /> +
+ ); +} + +export default ExampleComponent; +``` +
+ +--- + +

useEffect를 사용할 때 주의할 점

+ + 1. eslint-disable-line react-hooks/exhaustive-eps 주석 자제 + - useEffect 인수 내부에서 사용하는 값 중 의존성 배열에 있지 않는 경우 경고를 발생시켜주는데 의도치 못한 버그발생의 원인이 될 수 있다. + +
+ + 2. useEffect의 첫번째 인수에 함수명 부여 + - useEffect를 사용하는 많은 코드에서 useEffect의 첫 인수로 익명 함수를 부여한다. 하지만 useEffect의 수가 많아지거나 로직이 복잡해지면 적절한 이름을 붙여주자. + +```tsx +useEffect(function fetchData() { // 비동기 작업 처리 }, [dependency]); +``` + + 3. 거대한 useEffect를 만들지 말자 + - useEffect의 부수효과가 커질수록 성능에 악영향을 미친다. 큰 useEffect를 만들더라도 작은 useEffect들로 분리하는것이 좋다. + +
+ + 4. 불필요한 외부 함수를 만들지 말자 + - useEffect가 실행하는 콜백 또한 불필요하게 존재하면 안된다. + +```tsx +// 나쁜 예: 불필요한 외부 함수 생성 +const fetchData = () => { // 비동기 작업 처리 }; +useEffect(() => { fetchData(); }, [dependency]); + +// 좋은 예: 필요할 때만 함수 정의 +useEffect(() => { + const fetchData = () => { // 비동기 작업 처리 }; fetchData(); }, [dependency]); +``` + +
+ +--- + +

useEffect 콜백으로 비동기함수를 못넣는 이유

+ +```tsx +useEffect(() => { + async function fetchData() { + const result = await fetch(''); + setData(result.data); + } + + fetchData(); // 비동기 함수를 직접 호출 + + // 다음 코드는 fetchData가 완료되기를 기다리지 않고 실행될 수 있음 + console.log('Fetch 데이터 설정 후 실행'); + + // 콜백 내에서의 비동기 함수 호출은 정리 함수가 실행되기 전에 완료될 수 있음 + return () => { + console.log('정리 함수'); + }; +}, []); +``` +```tsx +useEffect(() => { + async function fetchData() { + const result = await fetch(''); + return result.data; + } + + fetchData().then(data => { + setData(data); // 데이터 설정 + }); + + return () => { + console.log('정리 함수'); + }; +}, []); +``` + +
+ +

useMemo

+ +- 비용이 큰 연산에 대한 결과를 저장(메모이제이션)해 두고, 이 저장된 값을 반환하는 훅 +생성함수, 배열을 인수로 받는다. + +- 렌더링 발생 시 의존성 배열의 값이 변경되지 않았으면 함수를 재실행하지 않고 이전에 기억해 둔 + +- 해당 값을 반환하고, 의존성 배열의 값이 변경됐다면 첫 번째 인수의 함수를 실행한 후에 그 값을 반환하고 그 값을 다시 기억해 둔다. + +
+ +

useCallback

+ +- 인수로 넘겨받은 콜백 자체를 기억한다. + +- 특정 함수를 새로 만들지 않고 다시 재사용한다. + +

memo를 사용함에도 전체 자식 컴포넌트가 리렌더링 되는 예제

+ +```tsx +import React, { memo, useEffect, useState } from "react"; + +const ChildComponent = memo(({ name, value, onChange }: any) => { + useEffect(() => { + console.log("render!!!!", name); + }); + + return ( + <> +

+ {name} {value ? "켜짐" : "꺼짐"} +

+ + + ); +}); + +const MyComponent = () => { + const [status1, setStatus1] = useState(false); + const [status2, setStatus2] = useState(false); + + const toggle1 = () => { + setStatus1(!status1); + }; + + const toggle2 = () => { + setStatus2(!status2); + }; + return ( +
+ + +
+ ); +}; + +export default MyComponent; +``` + +

Props 변경

+- MyComponent에서 ChildComponent에게 전달하는 name, value, onChange Props는 모두 부모 컴포넌트의 상태에 의존적입니다. 따라서 status1 또는 status2 상태가 변경될 때마다 해당 ChildComponent의 모든 Props가 변경된다. + +

메모이제이션된 Props

+- memo로 ChildComponent를 메모이제이션하더라도, 함수나 객체 같은 참조 타입의 Props가 변경될 때마다 새로운 Props 객체가 생성되므로, React.memo의 최적화 효과를 제대로 볼 수 없다. + +
+ +```tsx +const toggle1 = useCallback(() => { + setStatus1((prevStatus) => !prevStatus); +}, []); + +const toggle2 = useCallback(() => { + setStatus2((prevStatus) => !prevStatus); +}, []); +``` + +- Props가 변경되는 경우, 새로운 Props 객체가 생성되어 메모이제이션된 ChildComponent에 새로 전달될 수 있다. useCallback으로 toggle1 함수가 setStatus1을 호출할 때마다 새로운 함수가 생성되는 것을 막아 성능을 최적화한다. + +
+ +

useMemo vs. useCallback

+ +| 구분 | useMemo | useCallback | +|---|---|---| +| 목적 | 값 메모이징 | 함수 메모이징 | +| 사용 시기 | 성능 향상이 필요한 복잡한 연산 | 함수를 props로 전달할 때 | +| 예시 | 큰 데이터 세트에서 필터링/정렬 | 자식 컴포넌트에 콜백 함수 전달 | + +```tsx +import { useState, useMemo, useCallback } from 'react'; + +function MyComponent() { + const [numbers, setNumbers] = useState([1, 2, 3, 4, 5]); + const [count, setCount] = useState(0); + + // numbers 배열의 합계를 계산하고 메모이징한다. + // numbers 배열이 변경되지 않는 한 재계산되지 않는다. + const total = useMemo(() => { + console.log('총 계산 중...'); + return numbers.reduce((acc, num) => acc + num, 0); + }, [numbers]); + + // handleClick 함수를 메모이징한다. + // count 값이 변경되지 않는 한 새로운 함수가 생성되지 않는다. + const handleClick = useCallback(() => { + setCount(count + 1); + }, [count]); + + return ( +
+

총 합계: {total}

+ +

카운트: {count}

+
+ ); +} +``` + +

useRef

+ +- 컴포넌트 내부에서 렌더링이 일어나도 변경 가능한 상태값을 저장한다. + +- 반환값인 객체 내부에 있는 current로 값에 접근, 변경할 수 있다. + +- 값이 변하더라도 렌더링을 발생시키지 않는다. + +

useRef을 사용하는 이유

+ +1. DOM 접근 및 조작 + +```tsx + const inputRef = useRef(null); + // useRef를 사용해 input 요소에 대한 참조 생성 + + const focusInput = () => { + inputRef.current.focus(); // 버튼 클릭 시 input 요소에 포커스를 줌 + }; + + return ( + <> + + + + ); +``` + +
+ +2. 값의 변경 감지 및 보존 + - useRef는 값이 변경되더라도 컴포넌트가 다시 렌더링되지 않고 값을 유지할 수 있다. + +```tsx +const previousValueRef = useRef(value); +// useRef를 사용해 이전 값 저장 + + useEffect(() => { + if (previousValueRef.current !== value) { + // 이전 값과 현재 값 비교 + console.log('값이 변경되었습니다:', previousValueRef.current, '에서', value, '로'); // 값이 변경될 때 콘솔에 출력 + previousValueRef.current = value; // 이전 값 업데이트 + } + }, [value]); // value가 변경될 때 useEffect가 실행되도록 설정 + + return

현재 값: {value}

; +``` + +

useContext

+ +- 부모 컴포넌트에서 자식 컴포넌트로 데이터를 전달할 때 사용하는 훅 + +

Props Drilling

+ +- 여러 컴포넌트를 거쳐 데이터를 전달하는 것 중간에 위치한 컴포넌트에서는 필요없는 props를 전달해야할 수 있어 가독성이 떨어질 수 있다. → Context 등장 + +```tsx + + + + + + + + +``` + +

Context

+ +- props 전달 없이도 선언한 하위 컴포넌트 모두에서 자유롭게 원하는 값을 사용할 수 있다. + +```tsx +// ThemeContext.js +// createContext로 ThemeContext를 생성하고 기본값으로 'light'를 설정 +import { createContext } from 'react'; + +const ThemeContext = createContext('light'); + +export default ThemeContext; +``` +```tsx +// App.js +import React, { useState } from 'react'; +import ThemeContext from './ThemeContext'; +import ChildComponent from './ChildComponent'; + +const App = () => { + const [theme, setTheme] = useState('light'); + + return ( + // ThemeContext.Provider를 사용하여 하위 컴포넌트에 theme 값을 제공 + +
+

Current Theme: {theme}

+ + +
+
+ ); +}; + +export default App; +``` +```tsx +// ChildComponent.js +// useContext로 ThemeContext의 값을 읽어오기 +import React, { useContext } from 'react'; +import ThemeContext from './ThemeContext'; + +const ChildComponent = () => { + const theme = useContext(ThemeContext); + + return ( +
+

Current Theme: {theme}

+
+ ); +}; + +export default ChildComponent; +``` + +

usecontext를 사용할 때 주의할 점

+ +- useContext를 활용한 컴포넌트는 재활용이 어려워 진다. + +- 컨텍스트와 useContext는 상태관리를 위한 API가 절대 아니다. 컨텍스트는 상태를 주입해주는 API다. + +- 어떤 상태를 기반으로 다른 상태를 만들어 낼 수 있어야 한다. +필요에 따라 이런 상태변화를 최적화 할 수 있어야 한다.
+→ 상태관리 라이브러리를 위한 두가지 조건을 useContext는 충족시켜주지 못하고 단순히 props 값을 하위로 전달해 줄 뿐이다. + +
+ +

useReducer

+ +- useState의 심화 버전 (상태 관리를 위해 사용되는 훅) + +- 상태 관리가 복잡하고 여러 액션에 따라 상태를 업데이트해야 할 때 유용하다. + +- 반환값 + - state: 현재 useState가 가진 값 + - dispatcher: state를 업데이트 하는 함수 + +- 3개의 인수 + - reducer : useReducer의 기본 action을 정의하는 함수 + - initialState : useReducer의 초기값 + - init : 초기값을 지연해서 생성하고 싶을 때 사용하는 함수 + +```tsx +import React, { useReducer } from 'react'; + +// 초기 상태 정의 +const initialState = { count: 0 }; + +// 리듀서 함수 정의 +const reducer = (state, action) => { + switch (action.type) { + case 'increment': + return { count: state.count + 1 }; + case 'decrement': + return { count: state.count - 1 }; + case 'reset': + return initialState; + default: + throw new Error(); + } +}; + +const Counter = () => { + // useReducer로 상태와 리듀서를 연결 + const [state, dispatch] = useReducer(reducer, initialState); + + return ( +
+

Count: {state.count}

+ + + +
+ ); +}; + +export default Counter; +``` + +
+ +

useImperativeHandle

+ +- 부모에게서 넘겨받은 ref를 원하는 대로 수정할 수 있는 훅 + +

forwardRef

+ +- 리액트 컴포넌트에서 ref를 직접 전달할 수 있게 해주는 메커니즘 + +- 리액트에서 일반적으로 자식 컴포넌트에 직접적으로 ref를 전달할 수 없다. + +- forwardRef를 사용하면 부모 컴포넌트에서 자식 컴포넌트에 ref를 직접 전달할 수 있다. (DOM 요소에 직접 접근해야 할 때 유용) + +```tsx +import React, { forwardRef, useRef, useImperativeHandle } from 'react'; + +// forwardRef로 외부에서 ref를 직접 전달받을 수 있는 컴포넌트 생성 +const FancyInput = forwardRef((props, ref) => { + // useRef로 내부에서 관리할 변수 생성 + const inputRef = useRef(); + + // useImperativeHandle을 사용하여 외부로 노출할 메서드 정의 + useImperativeHandle(ref, () => ({ + focus: () => { + inputRef.current.focus(); // 내부 input 요소에 포커스 + } + })); + + return ; +}); + +// App 컴포넌트에서 FancyInput을 사용합니다. +const App = () => { + const inputRef = useRef(); + + const handleClick = () => { + // FancyInput 컴포넌트의 focus 메서드를 호출해 포커스 + inputRef.current.focus(); + + return ( +
+ + +
+ ); +}; + +export default App; +``` + +
+ +

useLayoutEffect

+ +- useEffect와 동일하나 모든 DOM의 변경 후에 동기적으로 발생한다. + +- 모든 DOM의 변경 후에 useLayoutEffect의 콜백 함수 실행이 동기적으로 발생한다. + +- DOM은 계산됐지만 화면에 반영되기 전에 하고싶은 작업이 있을 때 씀.
+ +ex) 사용자가 특정 버튼을 클릭하면 페이지의 특정 위치로 스크롤을 이동하고 싶은 경우, 특정 요소의 크기나 위치를 계산하여 다른 요소의 스타일을 업데이트해야 하는 경우, DOM 변경에 따른 애니메이션을 동기적으로 실행해야 하는 경우 + +1. 리액트가 DOM 업데이트 +2. **useLayoutEffect 실행** +3. 브라우저에 변경 사항 반영 +4. useEffect 실행 + +
+ +

useDebugValue

+ +- 컴포넌트의 디버깅을 돕기 위해 사용 + +- 사용자 정의 훅 내부의 내용에 대한 정보를 남길 수 있는 훅 + +- 두 번째 인수로 포매팅 함수를 전달하면 이에 대한 값이 변경됐을 때만 호출되어 포매팅된 값을 노출한다. + +```tsx +import { useDebugValue } from 'react'; + +function useCustomHook(value) { + // value 값에 따라 다른 디버그 레이블을 설정 + useDebugValue(value > 10 ? 'High' : 'Low'); + + // 실제 커스텀 훅의 로직 + // ... +} +``` + +

훅의 규칙

+ +- 최상위에서만 훅을 호출해야 한다. + +- 훅을 호출할 수 있는 것은 리액트 함수 컴포넌트, 사용자 정의 훅 두 가지 경우 뿐. + +

정리

+ +## 사용자 정의 훅 vs. 고차 컴포넌트 + +| 구분 | 사용자 정의 훅 | 고차 컴포넌트 | +|---|---|---| +| 개념 | 함수 컴포넌트에서 사용할 수 있는 재사용 가능한 로직을 캡슐화한 함수 | 다른 컴포넌트를 입력으로 받아 새로운 컴포넌트를 반환하는 함수 | +| 구현 방식 | 함수 컴포넌트 내부에서 다른 훅(`useState`, `useEffect` 등)을 호출하여 구현 | 기존 컴포넌트를 입력으로 받아 새로운 컴포넌트를 반환하는 함수로 구현 | +| 사용 방법 | 함수 컴포넌트 내에서 직접 호출하여 사용 | 기존 컴포넌트를 새로운 컴포넌트로 감싸서 사용 | +| 컴포넌트 구조 | 함수 컴포넌트 내부에 포함되어 있어 컴포넌트 트리에 영향을 미치지 않음 | 새로운 컴포넌트를 반환하므로 컴포넌트 트리에 영향을 미침 | +| 디버깅 | 함수 컴포넌트 내부에 있어 디버깅이 상대적으로 쉬움 | 새로운 컴포넌트가 생성되므로 디버깅이 상대적으로 복잡할 수 있음 | +