함수형 컴포넌트가 상태를 사용하거나,
클래스형 컴포넌트의 생명주기 메서드를 대체하는 등의 작업을 위해 훅 (hook) 을 사용하기 시작했다.
3.1 리액트의 모든 훅 파헤치기
useState, useEffect, useMemo, useCallback useRef, useContext,
useReducer, useImperativeHandle, useLayoutEffect, useDebugValue..
useState
useState : 함수형 컴포넌트 내부에서 상태를 정의하고 관리
import {useState} from 'react';
const [state, setState] = useState(initialState)
useState의 초기값이 복잡하거나 무거운 연산을 포함하고 있다면,
익명함수 () => 를 통해 변수 대신 함수를 실행해 값을 반환하는 것을 권장한다. (게으른 최적화)
이를 통해 initialState부문은 state가 처음 만들어질 때만 사용되고, 이후 랜더링이 발생하여도 무시된다.
게으른 최적화 사용 상황 예시
- localStorage나 sessionStorage에 대한 접근
- map, filter, find와 같은 배열에 대한 접근
- 초깃값 계산을 위해 함수 호출이 따로 필요할 때
useEffect
"애플리케이션 내 컴포넌트의 여러 값들을 활용해 동기적으로 부수 효과를 만듬"
렌더링 되거나, 값이 변경될 때, 특정 동작을 수행하도록 설정!
effect가 컴포넌트의 사이드 이펙트(부수효과)를 의미한다는 것을 명심하자.
function Component() {
// ...
useEffect(() => {
// do something
}, [props, state])
// ...
}
(콜백 함수, 의존성 배열)을 받아 의존성 배열의 값이 변경될 때마다 첫 번째 인수인 콜백을 실행한다.
(값이 변경되었는지는 렌더링 시 이전값과 얕은 비교를 수행)
클래스형 컴포넌트의 생명주기 메서드와 비슷한 작동을 할 수 있다.
클린업 함수를 반환할 수 있는데, 이는 컴포넌트가 언마운트될 때 실행된다.
*클린업 함수란?
함수형 컴포넌트가 리렌딩되었을 때 의존성 변화가 있었을 당시 이전의 값을 기준으로 실행한다.
특정 이벤트의 핸들러가 무한히 추가되는 것을 방지하고,
이전 상태 정리, 네트워크 요청 정리 및 리소스 해제의 역할을 한다.
의존성 배열 값에 따른 동작
- 빈 배열 ([]): 최초 렌더링 직후에만 콜백이 실행되고, 이후에는 실행되지 않습니다.
- 의존성 배열에 값이 있음: 배열에 있는 값이 변경될 때마다 콜백 함수가 실행됩니다.
- 의존성 배열이 없음: 컴포넌트가 렌더링될 때마다 콜백이 실행. 렌더링 여부 확인을 위해 주로 사용
! 사용 시 주의점
- 주식은 최대한 자제하라 ( eslint-disable-line reac hooks / exhaustive-deps과 같은 에러 발생 가능)
- useEffect의 첫 번째 인수에 함수명을 부여하라 (익명함수가 아닌 기명함수를 사용)
- 거대한 useEffect를 만들지 마라 (JS 실행 성능에 영향을 미치므로, 가급적 간결하고 가볍게)
- 불필요한 외부 함수를 만들지 마라 (내부에서 사용할 부수 효과라면 내부에서 정의해 사용하자)
- 콜백 함수로는 비동기 함수를 바로 넣을 수 없다! -> race condition
useMemo
비용이 큰 연산에 대한 결과를 저장해두고, 해당 값을 반환 (메모이제이션)
import{useMemo} from 'react';
const memoizedValue = useMemo(() => expensiveComputation(a,b), [a,b])
(값 반환을 위한 생성 함수, 해당 함수가 의존하는 값의 배열) 을 인수로 가진다.
의존성 배열의 값이 변경되지 않았다면, 기존 기억하고 있던 값을 반환
변경되었다면, 첫 번째 인자인 함수를 재실행후 반환 및 다시 기억
(값 뿐만 아니라 컴포넌트도 기억할 수 있지만, 그 때는 React.memo를 사용하는 것이 더 권장됨)
useCallback
useMemo가 값을 기억한 것과 달리, useCallback은 인수로 받은 콜백 자체를 기억한다. (함수에 대한 메모이제이션)
import React, { useState, useCallback } from 'react';
const Counter = () => {
const [count, setCount] = useState(0);
const increment = useCallback(() => {
setCount((prevCount) => prevCount + 1);
}, []);
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>Increment</button>
</div>
);
};
export default Counter;
=> 함수의 재생성을 막아 불필요한 리소스 또는 리렌더링을 방지할 수 있다.
useRef
useState와 동일하게 컴포넌트 내부에서 렌더링이 이러안도 변경 가능한 상태값을 저장한다는 공통점이 있다.
차이점으로는 current를 통해 값에 접근이 가능하며, 값 변경시에도 렌더링을 발생시키지 않는다는 점이 있다.
왜? 필요할까 사용하지 않고 외부에서 값을 선언하여 관리하였다면..
- 컴포넌트가 실행되어 렌더링되기 전에도 변수가 메모리 공간을 차지함
- 컴포넌트 인스턴스 하나당 하나의 값을 필요로 하는데, 충족시킬 수 없음
function RefComponent() {
const inputRef = useRef()
// 이때는 미처 렌더링이 실행되기 전(반환되기 전)이므로 undefined를 반환한다.
console.log(inputRef.current) // undefined
useEffect(() => {
console. log(inputRef.current) // <input type="text"x/input>
}, [inputRef])
return <input ref={inputRef} type="text" />
}
DOM에 접근하고 싶을 때 useRef를 주로 사용한다.
useContext
상위 컴포넌트에서 만들어진 Context를 함수형 컴포넌트에서 사용할 수 있도록 해줌 (props값을 하위로 전달)
import React, { createContext, useContext } from 'react';
// Context 생성
const MyContext = createContext<{ hello: string } | undefined>(undefined);
function ParentComponent() {
return (
<>
{/* 최상위 Provider */}
<MyContext.Provider value={{ hello: 'react' }}>
{/* 중첩된 Provider */}
<MyContext.Provider value={{ hello: 'javascript' }}>
<ChildComponent />
</MyContext.Provider>
</>
);
}
function ChildComponent() {
// Context 값 사용
const value = useContext(MyContext);
// 최하위 Provider의 값인 'javascript'가 출력됨
return <div>{value ? value.hello : '값 없음'}</div>;
}
여러 개의 Provider가 있다면, 가장 가까운 Provider의 값을 가져옴
주입된 상태를 사용할 수 있게 해줄 뿐, 렌더링 최적화에는 영향 X.
Provider에 의존성이 생기는 것을 인지하자.
=> 컨텍스트가 미치는 범위는 필요한 환경에서 최대한 좁게 만들어야 한다!
useReducer
useState의 심화 버전, 보다 복잡한 상태값에 대해 미리 정의한대로 관리할 수 있다.
const [state, dispatch] = useReducer(reducer, initialState, init);
- state: 현재 useReducer가 관리하고 있는 상태 값
- dispatcher: 상태 값을 업데이트하는 함수입니다. setState와 달리 action 객체를 전달받아 상태를 업데이트
- reducer: 상태 업데이트 로직을 정의하는 함수
state와 action 두 인자를 받아 새로운 상태를 반환. 이 반환된 상태값이 상태의 새로운 값이 됩니다. - initialState: 상태의 초기값
- init: 초기 상태를 설정하는 함수로, 선택적으로 사용 (게으른 초기화 적용)
기본적으로는 initialState가 그대로 초기 상태로 사용되지만, init 함수가 제공되면 초기값을 커스터마이즈
import React, { useReducer } from 'react';
// 초기 상태값
const initialState = { count: 0 };
// init 함수
function init(initialState) {
return { count: initialState.count + 5 };
}
// reducer 함수
function reducer(state, action) {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
case 'decrement':
return { count: state.count - 1 };
default:
throw new Error('Unknown action type');
}
}
function Counter() {
// useReducer에 init 함수 적용
const [state, dispatch] = useReducer(reducer, initialState, init);
return (
<div>
<p>Count: {state.count}</p>
<button onClick={() => dispatch({ type: 'increment' })}>Increment</button>
<button onClick={() => dispatch({ type: 'decrement' })}>Decrement</button>
</div>
);
}
export default Counter;
useImperativeHandle
부모 컴포넌트가 자식 컴포넌트의 내부 기능을 제어할 수 있도록 도와주는 훅
부모 컴포넌트에서 자식 컴포넌트의 참조(ref)를 통해 특정 메서드나 속성에 접근할 수 있게 해줌
주요 사용 시점
- 자식 컴포넌트의 특정 기능이나 속성을 부모 컴포넌트에서 노출시키고 싶을 때.
- 보통 `forwardRef`와 함께 사용하여, 자식 컴포넌트의 특정 내부 동작을 제어해야 할 때 사용
import React, { useImperativeHandle, forwardRef, useRef } from 'react';
// 자식 컴포넌트
const ChildComponent = forwardRef((props, ref) => {
const inputRef = useRef();
useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current.focus();
}
}));
return <input ref={inputRef} />;
});
// 부모 컴포넌트
function ParentComponent() {
const childRef = useRef();
const handleClick = () => {
childRef.current.focus();
};
return (
<div>
<ChildComponent ref={childRef} />
<button onClick={handleClick}>Focus Child Input</button>
</div>
);
}
export default ParentComponent;
위 예시에서 부모 컴포넌트는 `useImperativeHandle`을 통해 자식 컴포넌트의 `focus` 메서드를 직접 호출할 수 있습니다.
useLayoutEffect
"이 함수의 시그니처는 useEffect와 동일하나. 모든 DOM의 변경 후에 동기적으로 발생핸다."
`useLayoutEffect`는 리액트 훅 중 하나로, DOM이 업데이트된 후, 브라우저가 화면을 그리기 전에 실행
이는 `useEffect`와 유사하지만, 화면에 변화가 일어나기 전에 동기적으로 실행된다는 점에서 차이가 있다.
레이아웃을 변경하거나, DOM을 직접 조작할 때 주로 사용
주요 사용 시점
- DOM 업데이트가 완료된 직후, 렌더링 이전에 동기적으로 코드를 실행해야 할 때.
- 레이아웃을 수정하거나, DOM을 직접 변경할 필요가 있을 때.
- 애니메이션을 처리하거나 레이아웃을 기반으로 동작해야 할 경우.
반드시 필요한 경우만 사용하자...!
import React, { useLayoutEffect, useState, useRef } from 'react';
function LayoutEffectExample() {
const [width, setWidth] = useState(0);
const divRef = useRef();
// DOM이 렌더링된 후에 실행되며, 화면이 그려지기 전에 레이아웃을 측정
useLayoutEffect(() => {
const divWidth = divRef.current.offsetWidth;
setWidth(divWidth);
}, []);
return (
<div>
<div ref={divRef} style={{ width: '100px', height: '100px', background: 'lightblue' }}>
Box
</div>
<p>Box Width: {width}px</p>
</div>
);
}
export default LayoutEffectExample;
실행 순서
리액트가 DOM을 업데이트 -> useLayoutEffect를 실행 -> 브라우저에 변경 사항을 반영 -> useEffect를 실행 (있다면)
`useLayoutEffect`는 `div` 요소가 화면에 렌더링되기 전에 그 요소의 너비를 측정해 상태로 저장합니다.
`useEffect`를 사용하면 렌더링 후에 측정되지만, `useLayoutEffect`는 화면이 그려지기 전에 동기적으로 실행됩니다.
useDebugValue
`useDebugValue`는 주로 커스텀 훅을 만들 때 사용되며, 리액트 개발자 도구에서 디버깅 정보를 제공하기 위한 훅.
이 훅은 개발 시에만 동작하며, 실제 런타임에서는 아무런 영향을 주지 않는다.
이를 통해 커스텀 훅이 리액트 개발자 도구에서 어떤 값을 관리하고 있는지 쉽게 파악할 수 있다.
주요 사용 시점:
- 커스텀 훅을 만들고, 이를 디버깅할 때 훅의 상태나 동작을 개발자 도구에서 확인하고 싶을 때.
- 특정 상태나 값의 디버깅 정보를 제공하여 커스텀 훅의 동작을 추적할 때.
import React, { useState, useDebugValue } from 'react';
// 커스텀 훅
function useCount(initialValue) {
const [count, setCount] = useState(initialValue);
// 디버깅용 값 설정
useDebugValue(count > 5 ? 'High' : 'Low');
return [count, setCount];
}
function DebugValueExample() {
const [count, setCount] = useCount(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
export default DebugValueExample;
위 예시에서 useDebugValue는 count가 5 이상이면 'High', 그렇지 않으면 'Low'를 리액트 개발자 도구에 표시
이는 디버깅 목적에만 사용되며, 프로덕션에서는 아무런 영향을 미치지 않음
Hooks 정리.
훅 (hook) | 설명 | 사용 상황 |
useState | 상태를 관리하는 기본 훅 | 입력값, 카운터 등 상태를 관리할 때 |
useEffect | 사이드 이펙트 처리. 마운트 및 업데이트 시 실행되는 로직 정의 |
데이터 로드, 이벤트 리스너 추가, DOM 업데이트 등 |
useMemo | 복잡한 계산 결과를 메모이제이션 | 재계산을 피하여 성능을 최적화 할 때 |
useCallback | 함수 자체를 메모이제이션하여 함수 재생성을 방지 | 자식 컴포넌트에게 콜백 전달 시 |
useRef | DOM 요소나 값으 참조를 유지하기 위한 훅 렌더링과 상관없이 값을 저장할 수 있음 |
DOM에 직접 접근하거나 렌더링에 영향 없이 참조를 유지해야 할 때 |
useContext | Context API에서 데이터를 가져옴 | 전역 상태나 공통 데이터를 하위 컴포넌트에 전달할 때 (테마/사용자 인증 정보) |
useReducer | 복잡한 상태 로직 처리, reducer와 dispatch로 관리 | 상태 업데이트에 여러 action을 정의할때 |
useImperativerHandle | 자식 컴포넌트에서 부모에게 메서드나 속성을 노출 | 부모가 자식의 내부 DOM요소나 동작을 제어 (주로 forwardRef와 함께 사용) |
useLayoutEffect | DOM이 업데이트된 직후, 화면이 그려지기 전에 동기적으로 실행 |
화면 렌더링 이전에 DOM의 크기나 위치를 측정하거나 조정해야 할 때 |
useDebugValue | 커스텀 훅에서 디버깅 정보를 표시할 때 사용 | 디버깅 시 추가 정보 표시하고 싶을 때 |
3.1.2 훅의 규칙
- 최상위에서만 훅을 호출해야 한다.
반복문이나 조건문, 중첩된 함수 내에서 훅을 실행할 수 없다.
이 규칙을 따라야만 컴포넌트가 렌더링될 때마다 항상 동일한 순서로 훅이 호출되는 것을 보장한다. - 훅을 호출할 수 있는 것은 리액트 함수형 컴포넌트, 사용자 정의 훅 두 가지 경우뿐이다.
일반 자바스크립트 함수에서는 훅을 사용할 수 없다.
3.2 사용자 정의 훅과 고차 컴포넌트 중 무엇을 써야 할까?
중복 코드를 피해 효율적이고, 유지보수가 쉬운 코드를 작성하자.
재사용 할 수 있는 로직을 관리 하기 - 사용자 정의 훅, 고차 컴포넌트
3.2.1 사용자 정의 훅
리액트 훅의 이름은 use로 시작한다는 규칙이 있으며, 사용자 정의 훅도 이를 따라야 한다.
import { useState, useEffect } from 'react';
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchData = async () => {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error('Network response was not ok');
}
const result = await response.json();
setData(result);
} catch (error) {
setError(error.message);
} finally {
setLoading(false);
}
};
fetchData();
}, [url]);
return { data, loading, error };
}
export default useFetch;
복잡하고 반복되는 로직은 사용자 훅으로 간단하게 만들어 재사용할 수 있다.
3.2.2 고차 컴포넌트
HOC(Higher Order Component)는 컴포넌트 자체의 로직을 재사용하기 위한 방법이다.
리액트가 아닌 JS환경에서 널리 쓰이는 기법
React.memo가 가장 자주 쓰인다.
props의 변경이 없음에도 컴포넌트의 렌더링을 방지할 수 있다.
useMemo로 구현시에는 값을 반환받기에 JSX함수 방식이 아니라 {}를 사용한 할당식을 사용한다는 차이가 있다.
목적과 용도가 보다 뚜렷한 memo를 사용하는 것이 좋다!
const MemoizedComponent = React.memo(OriginalComponent);
직접 고차 컴포넌트 만들기
import React, { useState, useEffect } from 'react';
// 고차 컴포넌트 정의: 로딩 상태 처리
function withLoading(Component) {
return function EnhancedComponent({ isLoading, ...props }) {
if (isLoading) {
return <p>Loading...</p>;
}
return <Component {...props} />;
};
}
// 기본 컴포넌트: 데이터를 화면에 출력
function DataComponent({ data }) {
return (
<div>
<h1>{data.title}</h1>
<p>{data.body}</p>
</div>
);
}
// HOC 적용된 컴포넌트
const DataComponentWithLoading = withLoading(DataComponent);
function App() {
const [data, setData] = useState(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
fetch('https://jsonplaceholder.typicode.com/posts/1')
.then((response) => response.json())
.then((data) => {
setData(data);
setIsLoading(false);
});
}, []);
return <DataComponentWithLoading isLoading={isLoading} data={data} />;
}
export default App;
! 사용 시 주의점
부수 효과를 최소화해야 한다. 기존에 인수로 받는 컴포넌트의 props를 변경하지 않도록 해야한다.
여러개를 사용시 그 복잡성이 커짐에 주의하자. (최소한으로 사용하자)
3.2.3 사용자 정의 훅과 고차 컴포넌트 중 무엇을 써야 할까?
사용자 정의 훅을 사용하는 경우
- 리액트에서 제공하는 훅으로만 공통 로직을 격리할 수 있을 때
- 상태 관리 및 비즈니스 로직을 여러 컴포넌트에서 재사용할 때.
- 렌더링되지 않는 로직(API 호출, 이벤트 핸들러 등)을 재사용할 때.
고차 컴포넌트를 사용해야 하는 경우
- 렌더링에 영향을 미치는 로직이 존재하는 경우
- 컴포넌트에서 UI 관련 로직이나 렌더링 최적화를 할 때.
- 기능을 추가하거나, 특정 조건에 따라 렌더링 여부를 결정할 때.
구분 | 사용자 정의 훅 | 고차 컴포넌트 |
주요 사용 목적 | 상태 관리, 비즈니스 로직 및 사이드 이펙트 처리 | 렌더링 로직 제어, UI관련 기능 추가, 권한 제어 |
적용 방식 | 로직을 함수로 추상화하여 여러 컴포넌트에서 재사용 |
컴포넌트를 인수로 받아 새 컴포넌트를 반환, 렌더링 로직에 영향을 미침 |
상황 예시 | API 요청, 상태 관리, 이벤트 핸들러, 윈도우 이벤트 처리 |
UI 최적화, 렌더링 로직 캡슐화, props기반 기능 추가 |