## useState
useState는 함수 컴포넌트 내부에서 상태를 정의하고 관리할 수 있게 하는 훅이다.
### 구현 살펴보기
```tsx
function Component() {
let state = "john";
function changeName() {
state = "jane";
}
return (
<div>
<p>{state}</p>
<button onClick={changeName}>Change Name</button>
</div>
);
}
```
이렇게 하면 버튼을 눌러도 리렌더링이 발생하지 않는다. 이유는 리렌더링을 발생시키는 조건을 충족하지 않아서이다. ([[2.4 렌더링은 어떻게 일어나는가?#렌더링이 언제 발생하는가|렌더링을 일으키는 방법]]) 리렌더링을 발생시키기 위해서 useState의 setter를 실행시켜보자.
```tsx
function Component() {
const [_, triggerRender] = useState();
let state = "john";
function changeName() {
state = "jane";
triggerRender(undefined);
}
return (
<div>
<p>{state}</p>
<button onClick={changeName}>Change Name</button>
</div>
);
}
```
이렇게 setter를 실행시켰는데도 변경된 이름이 화면에 보이지 않는다. 이 이유는 리액트의 렌더링이 함수 컴포넌트에서 반환한 결과물인 return의 값을 비교해서 실행하기 때문이다. `changeName`에서 아무리 state를 변경하더라도 Component가 다시 실행할 때 state를 다시 "john"으로 초기화하기 때문에 return 결과에는 변함이 없어 화면 또한 변경되지 않는다.
새롭게 함수가 실행되더라도 그 값을 유지하기 위해서 react는 [[클로저 (Closure)|클로저]]를 활용한다. 클로저를 이용해 useState가 실행된 이후에도 내부 지역변수인 state를 계속 참조할 수 있다.
```ts
const MyReact = (function () {
const global = {} as any;
let index = 0;
function useState(initialValue) {
if (!global.states) {
// 애플리케이션 전체의 states 초기화
global.states = [];
}
// 현재 상태값이 없다면 초깃값으로 설정한다.
const currentState = global.states[index] || initialValue;
global.states[index] = currentState;
// ⭐ 즉시 실행 함수로 setter를 만든다.
const setState = (function () {
// ⭐ 현재 index를 클로저로 가둬서 이후에도 접근 가능하도록 한다.
const currentIndex = index; // ⭐️
return function (newValue) {
global.states[currentIndex] = newValue; // ⭐️
};
})();
index++; // 다음 useState 호출을 위해 인덱스 증가
return [currentState, setState];
}
// useState를 사용하는 컴포넌트
function Component() {
const [value, setValue] = useState(0);
// ...
}
})();
```
실제 구현체에서는 [[#useReducer]]를 이용해서 useState를 구현한다.
> [!note]
> react 내부 구현을 살펴보기 위해서 깃허브에서 코드를 타고 올라가다보면 [`SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED`](https://github.com/search?q=repo%3Afacebook%2Freact+__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED&type=code)로 귀결된다고 한다. 이는 일반 사용자의 접근을 차단하고 실제 프로덕션 코드에서 사용하지 못하도록 막기 위함인데, 리액트 팀에서도 이쪽에 접근하는 것을 권장하지 않는다고 한다. 따라서 책에서 다루고 있는 훅의 구현은 리액트의 경량화 버전인 [Preact](https://github.com/preactjs/preact)의 구현을 기준으로 한다. Preact는 경량화 버전이지만 대부분의 리액트 API를 지원하고 있을 뿐 아니라 모든 코드를 명확하게 공개하고 있다.
> [preact/hooks/src/index.js at main · preactjs/preact · GitHub](https://github.com/preactjs/preact/blob/main/hooks/src/index.js#L172)
### 게으른 초기화
일반적으로는 useState에 기본값을 선언하기 위해서 인수로 원시값을 넣는 경우가 대부분일 것이다. 그러나 인수로 특정한 값을 넘기는 함수를 인수로 넣어줄 수도 있다. 이렇게 useState의 초깃값으로 변수 대신 함수를 넘기는 것을 게으른 초기화(lazy initialization)라고 한다.
```ts
// ⭐️ 매 렌더링마다 함수가 실행됨
const [state, setState] = useState(initialize());
// ⭐️ 초기 렌더링에만 실행됨
const [state, setState] = useState(initialize);
```
[초기 state 다시 생성하지 않기 – useState - React](https://ko.react.dev/reference/react/useState#avoiding-recreating-the-initial-state)
초깃값이 복잡하거나 무거운 연산을 포함하고 있을 때 사용하면 좋다. 게으른 초기화 함수는 state가 처음 만들어질 때만 실행되고 이후에 리렌더링이 발생하다면 이 함수의 실행은 무시된다.
```tsx
import { useState } from "react";
function initialize() {
console.log("초기화 작업 수행 (함수)");
return 0;
}
function Component() {
// ⭐️ 바로 초깃값을 설정한다.
const [state, setState] = useState(initialize());
// ⭐️ 함수를 통해 초깃값을 설정한다.
const [lazyState, setLazyState] = useState(() => {
console.log("초기화 작업 수행 (지연 함수)");
return 0;
});
function updateState() {
setState((prev) => prev + 1);
}
function updateLazyState() {
setLazyState((prev) => prev + 1);
}
return (
<div>
<div>
<h2>즉시 초기화</h2>
<p>state: {state}</p>
<button onClick={updateState}>state 증가</button>
</div>
<div style={{ marginTop: 20 }}>
<h2>지연 초기화</h2>
<p>lazyState: {lazyState}</p>
<button onClick={updateLazyState}>lazyState 증가</button>
</div>
</div>
);
}
```
![[3fa398a9-50bf-47e9-bc11-9016306977ae.png]]
리액트에서는 렌더링이 실행될 때 마다 함수 컴포넌트의 함수가 다시 실행된다는 점을 기억하자. 그러니까 useState의 값도 재실행된다. (초깃값을 새로 설정하는 것은 아니다. [[#구현 살펴보기]]에서 봤듯이 초깃값이 없을 때만 설정한다.) 그래서 만약 초깃값을 만드는데 많은 비용이 드는 작업이 있다면 함수로 넘겨주는 것이 훨씬 더 경제적이다.
게으른 최적화는 무거운 연산이 요구될 때 사용하면 좋다. 즉 localStorage나 sessionStorage 같은 브라우저 스토리지에 접근할때나 map, filter, find같이 배열에 접근해야 할 때, 또는 초깃값 설정을 위해 함수 호출이 필요한 경우 같이 실행 비용이 많이 드는 경우에 사용하면 된다.
## useEffect
useEffect는 애플리케이션 내 컴포넌트의 여러 값들을 활용해 비동기적으로 부수 효과를 만드는 메커니즘이다.
### useEffect란?
```ts
useEffect(() => {
// do something
}, [props, state]);
```
첫번째 인수로 실행할 부수 효과가 포함된 함수를, 두번째 인수로는 의존성 배열을 전달한다. 의존성 배열이 변경될 때 마다 첫번째 인수인 콜백을 실행한다.
```tsx
function Component() {
const [count, setCount] = useState(0);
useEffect(() => {
console.log(`count is: ${count}`);
}, [count]);
function increaseCount() {
setCount((prev) => prev + 1);
}
return (
<div>
<p>Count: {count}</p>
<button onClick={increaseCount}>Increment</button>
</div>
);
}
```
함수 컴포넌트는 매 렌더링마다 함수를 실행한다. 이 때 useEffect는 의존성에 있는 값을 보면서 이 의존성의 값이 이전과 다른 게 하나라도 있으면 부수 효과를 실행하는 식으로 동작한다.
### 클린업 함수의 목적
```tsx
function Component() {
const [count, setCount] = useState(0);
useEffect(() => {
console.log(`count is: ${count}`);
return () => {
console.log(`cleaning up count: ${count}`);
};
}, [count]);
function increaseCount() {
setCount((prev) => prev + 1);
}
return (
<div>
<p>Count: {count}</p>
<button onClick={increaseCount}>Increment</button>
</div>
);
}
```
![[249d3ebc-1e7d-4e2b-bf77-8671d7074015.png]]
useEffect에서 반환하는 함수를 보통 클린업 함수라고 한다. 위 로그를 살펴보면 클린업 함수는 이전 state를 이용해서 새로운 랜더링 뒤에 실행된다. 중요한 것은 새로운 값으로 렌더링 한 이후에 이전 값을 보고 실행한다는 것이다. 함수 컴포넌트의 useEffect는 콜백을 실행할 때 마다 이전의 클린업 함수가 존재한다면 그 클린업 함수를 실행한 뒤에 콜백을 실행한다. 이런 원리로 이벤트를 추가하기 이전이 등록했던 이벤트 핸들러를 삭제하는 코드를 클린업 함수에 추가하는 것이다. 이렇게 함으로써 이벤트 핸들러가 무한히 추가되는 것을 막을 수 있다.
이런 면에서 클린업 함수는 생명주기 메서드의 언마운트 개념과는 좀 다르다. 언마운트는 특정 컴포넌트가 DOM에서 사라지는 것을 의미하는데, 클린업 함수는 언마운트라기보다는 함수 컴포넌트가 리렌더링되었을 때 이전의 값을 기준으로 실행되는, 이전 상태를 청소해주는 개념으로 보는 것이 옳다.
### 의존성 배열
- 의존성 배열에 빈 배열을 둔다면 비교할 의존성이 없다고 판단해 최초 렌더링 직후에 콜백을 실행된 다음부터는 더 이상 실행되지 않는다.
- 아무런 값도 넘겨주지 않는다면 의존성을 비교하지 않고 매 렌더링이 발생할 때 마다 콜백을 실행한다.
그런데 의존성이 없는 useEffect는 일반 코드와 비슷한게 아닌가?
```tsx
function Component() {
useEffect(() => {
console.log("컴포넌트 렌더링됨");
});
// ...
}
function Component() {
console.log("컴포넌트 렌더링됨");
// ...
}
```
둘의 차이점은 다음과 같다.
1. 서버 사이드 렌더링 관점에서 useEffect는 클라이언트 사이드에서 실행되는 것을 보장한다. 따라서 useEffect 내부에서는 window 객체에 접근하는 코드를 사용해도 안전하다.
2. useEffect는 컴포넌트 렌더링이 완료된 이후에 실행된다. 반면에 컴포넌트 직접 실행은 컴포넌트가 렌더링되는 도중에 실행된다. 그래서 이 작업은 함수 컴포넌트의 반환을 지연시킬 수 있기 때문에 무거운 작업일 경우 렌더링을 방해해서 성능에 악영향을 미칠 수 있다.
useEffect에서 effect는 부수효과를 의미한다. 즉 **컴포넌트가 렌더링된 이후**에 어떠한 부수 효과를 일으키고 싶을 때 사용하는 훅이다.
### useEffect의 구현
[preact/hooks/src/index.js at main · preactjs/preact · GitHub](https://github.com/preactjs/preact/blob/main/hooks/src/index.js#L278)
```ts
const MyReact = (function () {
const global = {} as any;
let index = 0;
function useEffect(callback, dependencies) {
const hooks = global.hooks;
// 이전 훅 정보가 있는지 확인한다.
const previousDependencies = hooks[index];
// 이전 값이 있다면 이전 값을 얕은 비교로 비교해서 변경이 일어났는지 확인한다.
// 이전 값이 없다면 최초 실행이므로 변경이 일어난 것으로 간주해서 콜백을 실행하도록 한다.
const isDependenciesChanged =
!previousDependencies ||
dependencies.some(
(dep, idx) => !Object.is(dep, previousDependencies[idx]) // ⭐️
);
index++; // ⭐️
if (isDependenciesChanged) {
callback();
// 현재 훅의 의존성 배열을 저장한다.
hooks[index] = dependencies;
}
}
})();
```
핵심은 의존성 배열의 이전 값과 현재 값의 얕은 비교다. 이전 의존성 배열과 현재 의존성 배열의 값에 하나라도 변경 사항이 있다면 콜백을 실행한다. 이것이 useEffect의 본질이다.
### useEffect를 사용할 때 주의할 점
**1. `eslint-disable-line react-hooks/exhaustive-deps` 주석은 최대한 자제한다.**
react-hooks/exhaustive-deps 규칙은 useEffect에서 사용하는 값 중 의존성 배열에 포함되어 있지 않은 값이 있을 때 경고를 발생시킨다.
정말로 필요한 경우에는 사용할 수도 있지만 대부분의 경우에는 의도치 못한 버그를 만들 가능성이 높다. 대부분의 경우에는 의존성 배열로 빈 배열을 전달해서 컴포넌트가 마운트되는 시점에 무언가를 하고싶은 경우에 이 주석을 이용해서 빈 배열을 전달하곤 한다. 하지만 이것은 클래스 컴포넌트의 componentDidMount에 기반한 접근법으로 가급적이면 사용해선 안된다.
useEffect는 의존성 배열로 전달한 값의 변경에 의해서 실행되도록 설계된 훅이다. 그러나 의존성 배열에 존재하지 않는 값을 사용한다는 것은 이 부수효과가 실제로 관찰해야 하는 값과는 별개로 동작한다는 것이다. 즉 state, props와 같은 어떤 값의 변경과는 별개로 useEffect의 부수 효과가 작동한다는 것이다.
따라서 정말로 의존성으로 `[]` 가 필요하다면 최초에 함수 컴포넌트가 마운트된 시점에 콜백 함수 실행이 필요한지를 다시 한번 확인해봐야 한다. 만약 정말 그렇다면 useEffect 내 부수 효과가 실행될 위치가 잘못되었을 가능성이 크다. 예를 들어보자.
```tsx
function Component({log}: {log: string}) {
useEffect(() => {
logging(log);
}, []); // eslint-disable-line react-hooks/exhaustive-deps
}
```
위 코드는 log가 최초로 props로 넘어와서 컴포넌트가 최초로 렌더링 된 시점에만 실행된다. 의도는 아마도 컴포넌트가 최초로 렌더링되었을 때 logging을 실행하려고 한 것일것이다.
하지만 위 코드는 당장은 아니더라도 버그의 위험성을 가지고 있다. log가 아무리 변하더라도 부수 효과가 실행되지 않아 useEffect의 실행 흐름과 log의 변경 흐름이 별개로 동작하게 된다. 따라서 어쩌면 logging 호출은 자식 컴포넌트가 아닌 log를 전달하는 부모 컴포넌트 쪽에서 전달하는 것이 옳을지도 모른다.
이처럼 useEffect에 빈 배열을 넘기기 전에는 정말로 useEffect의 부수 효과가 실제로 컴포넌트 상태와 별개로 동작해야 하는지, 이 방법이 최선인지를 한번 더 검토해봐야 한다. 빈 배열이 아닐 때에도 마찬가지이다. 만약 특정 값을 사용하지만 해당 값의 변경 시점을 피할 목적이라면 메모이제이션을 적절히 활용해 해당 값의 변화를 막거나 적당한 실행 위치를 한번 더 고민해보는 것이 좋다.
**2. 콜백 함수로 기명 함수를 사용하자**
useEffect를 사용하는 많은 코드에서 첫번째 인수로 익명 함수를 전달한다. useEffect의 수가 적거나 복잡도가 낮다면 큰 문제는 없겠지만 useEffect 코드가 많아지고 복잡해지면 무슨 일을 하는 useEffect 코드인지 파악하기 어려워진다. 이 때 콜백 함수를 익명이 아닌 기명으로 바꾸어주는 것이 좋다. 이렇게 이름을 붙이면 useEffect의 목적을 파악하기 쉬워진다.
```ts
useEffect(function logUserId() {
logging(user.id);
}, [user.id]);
```
**거대한 useEffect를 만들지 마라**
useEffect는 의존성이 변경될 때 마다 부수 효과를 실행하는데, 이 부수효과가 커질수록 애플리케이션 성능에 악영향을 미친다. useEffect는 렌더링 이후에 실행되기 때문에 렌더링 자체에는 영향을 적게 미칠 수 있지만 여전히 자바스크립트 실행 엔진에는 영향을 미친다. 그래서 가능한 useEffect는 간결하고 가볍게 유지하는 것이 좋다. 부득이하게 큰 useEffect를 만들어야 한다면 적은 의존성 배열을 사용하는 여러 개의 useEffect로 분리하는 것이 좋다.
```tsx
// ❌ 거대한 useEffect
useEffect(() => {
fetchUserData();
fetchPosts();
fetchComments();
setupWebSocket();
trackAnalytics();
}, [userId, postId, commentId, wsConfig, trackingId]);
// ✅ 분리된 useEffect
useEffect(() => {
fetchUserData();
}, [userId]);
useEffect(() => {
fetchPosts();
}, [postId]);
useEffect(() => {
const ws = setupWebSocket();
return () => ws.close();
}, [wsConfig]);
```
> [!tip] 왜?
만약에 의존성 배열 안에 불가피하게 많은 변수들이 들어가야 한다면 최대한 useCallback이나 useMemo 등으로 정제한 내용들만 useEffect에 담아두는 것이 좋다.
**불필요한 외부 함수를 만들지 마라**
useEffect 안에서 사용하는 함수는 내부로 가져오는 것이 좋다. 이렇게 하면 불필요한 의존성 배열도 줄일 수 있고 무한 루프로 빠지지 않도록 사용하는 useCallback도 사용하지 않아도 된다. useEffect 안에서 사용할 부수효과는 내부에서 만들어서 정의해 사용하는 것이 더 좋다.
> [note] 왜 useEffect의 콜백 함수로 비동기 함수를 바로 넣을 수 없을까?
> 만약 useEffect의 인수로 비동기 함수를 사용할 수 있다면 비동기 함수의 응답 속도에 따라 결과가 이상하게 나올 수 있다. 이런 문제를 useEffect의 경쟁 상태라고 한다.
> 하지만 직접 지정할 수 없을 뿐이지 사용할 수 없는 것은 아니다.
> ```ts
> useEffect(() => {
> let shouldIgnore = false; // ⭐️
>
> async function fetchData() {
> const response = await fetch("/api/data");
> const data = await response.json();
>
> if (!shouldIgnore) { // ⭐️
> // 상태 업데이트 로직
> }
> }
>
> fetchData();
>
> return () => {
> // 앞선 요청이 있었음을 표시함. (추가 실행 방지)
> shouldIgnore = true; // ⭐️
> };
> }, []);
> ```
> 비동기 함수가 내부에 존재하게 되면 비동기 함수가 반복해서 생성되고 실행될 수 있기 때문에 클린업 함수 내에서 이전 비동기 함수에 대해 처리해주는 것이 좋다. fetch의 경우에는 abortController를 활용하는 것도 방법이다. (abortController.abort())
> 즉 비동기 useEffect는 state의 경쟁 상태를 야기할 수 있고 cleanup 함수의 실행 순서도 보장할 수 없기 때문에 비동기 함수를 인수로 받지 않지 않는다고 볼 수 있다.
## useMemo
useMemo는 비용이 큰 연산에 대한 결과를 메모이제이션해두고 이 저장된 값을 반환하는 훅이다.
[preact/hooks/src/index.js at main · preactjs/preact · GitHub](https://github.com/preactjs/preact/blob/main/hooks/src/index.js#L342)
```ts
const memoizedValue = useMemo(() => expensiveComputation(a, b), [a, b]);
```
첫번째 인수로는 어떤 값을 반환하는 생성 함수를 , 두번째 인수로는 해당 함수가 의존하는 값의 배열을 전달한다. useMemo는 렌더링 발생 시에 의존성이 변경되지 않았다면 함수를 재실행하지 않고 기억해 둔 값을 반환한다. 이런 메모이제이션은 값 뿐 아니라 컴포넌트도 가능하지만 컴포넌트 메모이제이션에는 React.memo를 사용하는 것이 좀 더 현명하다.
useMemo 사용에 대한 내용 : [[2.5 컴포넌트와 함수의 무거운 연산을 기억해 두는 메모이제이션]]
## useCallback
useCallback은 인수로 넘겨받은 콜백 자체를 기억한다. 쉽게 말해서 특정 함수를 새로 만들지 않고 다시 재사용한다는 것을 의미한다. 함수를 메모해야 하는 상황의 가장 좋은 예로는 함수를 props로 전달하는 상황일 것이다.
컴포넌트 안에서 함수를 정의했다면, 컴포넌트가 렌더링 될 때 마다 함수가 재생성된다. 이 함수가 자식 컴포넌트에 props로 전달된다면 아무리 memo로 메모이제이션해두었더라도 부모 컴포넌트가 재렌더링 될 때 마다 자식 컴포넌트가 재렌더링될 것이다. 이 때 useCallback을 이용해서 함수를 메모이제이션한다면 의존성 배열이 변경되지 않는 한 함수를 재생성하지 않는다.
[preact/hooks/src/index.js at main · preactjs/preact · GitHub](https://github.com/preactjs/preact/blob/main/hooks/src/index.js#L359)
useCallback은 useMemo로 구현되어 있다.
```js
export function useCallback(callback, args) {
currentHook = 8;
return useMemo(() => callback, args); // ⭐️
}
```
useCallback과 useMemo의 유일한 차이는 메모이제이션 대상이 함수나 변수냐일 뿐이다. 자바스크립트에서는 함수도 값으로 표현될 수 있기 때문에 자연스러운 모습이다. 하지만 순수 useMemo로 함수를 메모이제이션하는 코드는 불필요하게 길기 때문에 따로 API를 제공하는 것으로 볼 수 있다.
```jsx
const handleClick1 = useCallback(() => {
setCounter((prev) => prev + 1);
}, []);
const handleClick2 = useMemo(() => {
return () => setCounter((prev) => prev + 1); // ⭐️
}, []);
```
위 두개는 모두 동일한 기능을 하지만 useMemo는 값을 메모이제이션 하는 기능이기 때문에 함수를 반환하는 함수를 만들어야 한다. 이는 코드를 작성하거나 리뷰하는 입장에서 혼란스러울 수 있으므로 직관적인 방법인 useCallback을 사용하는 것이 좋다.
useCallback 사용에 대한 내용 : [[2.5 컴포넌트와 함수의 무거운 연산을 기억해 두는 메모이제이션]]
## useRef
useRef는 useState와 동일하게 컴포넌트 내부에서 렌더링이 일어나도 값을 저장한다는 공통점이 있다. 하지만 useState와 구별되는 큰 차이점 두 가지를 갖고 있다.
- useRef는 반환값인 객체 내부에 있는 current로 값에 접근 또는 변경할 수 있다.
- useRef는 그 값이 변하더라도 렌더링을 발생시키지 않는다.
useRef는 왜 필요할까? 렌더링에 영향을 주지 않는 값이 필요한거라면 컴포넌트 외부에 값을 선언해서 관리하는 것도 동일하지 않을까?
```tsx
let value = 0;
function Component() {
function handleClick() {
value += 1;
}
}
```
이 방식의 단점은
- 컴포넌트가 실행되어 렌더링이 되지 않았음에도 value라는 값이 기본적으로 존재한다 => 메모리에 불필요한 값이 올라가게 하는 악영향이 있다.
- Component가 여러 번 생성된다면 각 컴포넌트에서 가리키는 값이 모두 value로 동일하다. 컴포넌트별로 동일한 값을 봐야 하는 경우라면 괜찮겠지만, 대부분의 경우에는 컴포넌트 인스턴스 하나당 하나의 값을 필요로 한다.
useRef를 이용하면 컴포넌트가 렌더링 될 때만 값이 생성되며 컴포넌트 인스턴스가 여러 개라도 각각 별개의 값을 바라본다. useRef의 가장 일반적인 사용 예는 DOM에 접근하는 경우이다.
```jsx
function Component() {
const inputRef = useRef();
// 렌더링이 실행되기 전이므로 undefined를 출력
console.log(inputRef.current);
useEffect(() => {
// 렌더링이 완료된 후를 보장하므로 input 요소가 출력됨
console.log(inputRef.current);
}, [inputRef]);
return <input ref={inputRef} type="text" />;
}
```
참고: [[#useEffect]]
한 가지 명심해야 할 것은 useRef의 최초 기본값은 return문에 정의해 둔 DOM이 아니라 useRef()에서 넘겨받은 인수라는 것이다. 따라서 useRef가 선언된 당시에는 아직 컴포넌트가 렌더링되기 전이라서 undefined 값을 갖고 있다.
useRef를 사용할 수 있는 유용한 경우는 렌더링을 발생시키지 않고 원하는 상태값을 저장할 수 있다는 특징을 활용해서 useState의 이전 값을 저장하는 usePrevious 같은 훅을 구현할 때이다.
```jsx
function usePrevious(value) {
const ref = useRef();
// ⭐️ useEffect는 렌더링 후에 실행되므로
// ⭐️ 이 시점에 ref.current에 값을 저장하면
// ⭐️ 다음 렌더링에서 "이전 값"으로 읽을 수 있다.
useEffect(() => {
ref.current = value;
}, [value]);
// 렌더링 중에는 ref.current가 아직 업데이트되지 않았으므로
// 이전 렌더링의 value를 반환한다.
return ref.current;
}
function Component() {
const [count, setCount] = useState(0);
const prevCount = usePrevious(count);
function increment() {
setCount((c) => c + 1);
}
return (
<div>
<p>Current Count: {count}</p>
<p>Previous Count: {prevCount === undefined ? "N/A" : prevCount}</p>
<button onClick={increment}>Increment</button>
</div>
);
}
```
> **⭐️ 실행 순서**
> 1. 렌더링: count = 0, prevCount = undefined (ref.current = undefined)
> 2. useEffect 실행: ref.current = 0
>
> \[버튼 클릭\]
>
> 3. 렌더링: count = 1, prevCount = 0 (ref.current = 0, 아직 안 바뀜)
> 4. useEffect 실행: ref.current = 1
>
> \[버튼 클릭\]
>
> 5. 렌더링: count = 2, prevCount = 1 (ref.current = 1, 아직 안 바뀜)
> 6. useEffect 실행: ref.current = 2
![[14f32e07-29e4-496d-880b-50b9b552fe92.png]]
이렇게 원하는 시점의 값을 렌더링에 영향을 미치지 않고 보관할 수 있다.
useRef는 useMemo를 이용해서 구현할 수 있다.
[preact/hooks/src/index.js at main · preactjs/preact · GitHub](https://github.com/preactjs/preact/blob/main/hooks/src/index.js#L306)
```js
export function useRef(initialValue) {
currentHook = 5;
// ⭐️ 빈 의존성 배열 -> 최초 렌더링에만 객체를 생성한다
// ⭐️ 이후 렌더링에서는 같은 객체 참조를 반환한다.
return useMemo(() => ({ current: initialValue }), []);
}
```
렌더링에 영향을 미치면 안 되기 때문에 useMemo에 의도적으로 빈 배열을 의존성으로 주었고, 이 덕분에 각 렌더링마다 동일한 객체를 가리킬 수 있다.
참고 : [Referencing Values with Refs – React](https://react.dev/learn/referencing-values-with-refs#differences-between-refs-and-state)
## useContext
### Context란?
리액트 애플리케이션은 기본적으로 트리 구조를 갖고 있기 때문에 부모가 가지고 있는 데이터를 자식에서도 사용하고 싶다면 props로 데이터를 넘겨주는 것이 일반적이다. 그러나 전달해야 하는 데이터가 있는 컴포넌트와 전달받아야 한는 컴포넌트의 거리가 멀어질수록 코드는 복잡해진다.
```jsx
<A props={something}>
<B props={something}>
<C props={something}>
<D props={something} />
</C>
</B>
</A>
```
이렇게 A 컴포넌트에서 제공하는 데이터를 D 컴포넌트에서 사용하려면 props를 필요한 위치까지 하위 컴포넌트로 계속해서 넘겨야 한다. 이런 것을 props drilling 이라고 한다.
props drilling은 데이터를 제공하는 쪽이나 받는 쪽 둘 다에게 불편하다. 해당 값을 사용하지 않는 컴포넌트에서도 단순히 전달하기 위해 props가 열려 있어야 하고, 사용하는 쪽에서도 제대로 props drilling이 되는지를 확인해야 해서 번거로운 작업이다.
이렇게 props drilling이 깊어지는 것을 극복하기 위해서 등장한 개념이 context이다. context를 이용하면 명시적인 props drilling 없이도 선언한 하위 컴포넌트 모두에서 자유롭게 원하는 값을 사용할 수 있다.
### Context를 함수 컴포넌트에서 사용할 수 있게 해주는 useContext
```tsx
const Context = createContext<{ value: string }>({ value: "hello" });
function ParentComponent() {
return (
<>
<Context.Provider value={{ value: "hello" }}>
<Context.Provider value={{ value: "world" }}>
<Component />
</Context.Provider>
</Context.Provider>
</>
);
}
function Component() {
const context = useContext(Context);
// hello가 아닌 world가 출력된다.
return <>{context.value ? context.value : "n/a"}</>;
}
```
useContext는 상위 컴포넌트에서 만들어진 Context를 함수 컴포넌트에서 사용할 수 있도록 만들어진 훅이다. useContext를 사용하면 상위 컴포넌트 어딘가에서 선언된 `<Context.Provider/>` 에서 제공한 값을 사용할 수 있게 된다. 만약에 여러 개의 Provider가 있다면 가장 가까운 Provider의 값을 가져온다.
컴포넌트 트리가 복잡해질수록 Context도 복잡해질 것이다. useContext로 Context를 사용하려고 했는데 정작 컴포넌트가 실행될 때 이 Context가 존재하지 않아서 에러가 발생할수도 있다. 이 문제를 방지하기 위해서는 useContext에서 해당 Context가 존재하는 환경인지 확인해 보면 된다.
```tsx
const Context = createContext<{ value: string }>({ value: "hello" });
function ContextProvider({
children,
value,
}: PropsWithChildren<{ value: string }>) {
return <Context.Provider value={{ value }}>{children}</Context.Provider>;
}
function useMyContext() {
const context = useContext(Context);
if (context === undefined) { // ⭐️
throw new Error(
"useMyContext는 ContextProvider 내부에서만 사용될 수 있습니다."
);
}
return context;
}
function ParentComponent() {
return (
<>
<ContextProvider value={"hello"}>
<ContextProvider value={"world"}>
<Component />
</ContextProvider>
</ContextProvider>
</>
);
}
function Component() {
const context = useMyContext();
return <>{context.value}</>;
}
```
이렇게 다수의 Provider와 useContext를 사용할 때에는 한번 더 감싸서 사용하는 것이 좋다. 타입 추론에도 유용하고 Provider 밖에서 useContext를 사용하는 예상치 못한 에러도 방지할 수 있다.
### useContext를 사용할 때 주의할 점
useContext를 사용하는 컴포넌트는 상위의 Provider에 의존적이므로 컴포넌트 재사용이 어려워진다는 점에 주의해야 한다. 이러한 상황을 해결하려면 useContext를 사용하는 컴포넌트를 최대한 작게 하거나 재사용되지 않을 컴포넌트에서만 useContext를 사용해야 한다.
이러한 문제를 해결하기 위해서 모든 Context를 최상위 루트 컴포넌트에 넣는 것은 어떨까? 앞서 언급한 에러는 줄어들 수 있지만 리액트 애플리케이션 관점에서는 그렇게 좋은 접근은 아니다. props로 제공해야 하는 컴포넌트가 많아지기 때문에 불필요하게 리소스가 낭비된다. 따라서 Context의 범위는 필요한 환경에만 최대한 좁게 만드는 것이 좋다.
---
일부 리액트 개발자들은 Context를 상태 관리를 위한 리액트의 API로 오해하고 있다. 엄밀히 따지면 Context는 상태를 주입해주는 API이다. 상태 관리 라이브러리가 되기 위해서는 최소한 아래 두 가지 조건을 만족해야 한다.
1. 어떠한 상태를 기반으로 다른 상태를 만들어 낼 수 있어야 한다.
2. 필요에 따라 이러한 상태 변화를 최적화할 수 있어야 한다.
그러나 Context는 둘 중 어느 것도 하지 못한다. 단순히 props 값을 하위로 전달할 뿐 useContext를 이용한다고 해서 렌더링이 최적화되지는 않는다.
```tsx
function GrandChildComponent() {
const { value } = useMyContext();
useEffect(() => {
console.log("GrandChildComponent 렌더링");
});
return <>{value}</>;
}
function ChildComponent() {
useEffect(() => {
console.log("ChildComponent 렌더링");
});
return <GrandChildComponent />;
}
function ParentComponent() {
const [text, setText] = useState("initial");
function handleChangeText(e: ChangeEvent<HTMLInputElement>) {
setText(e.target.value);
}
useEffect(() => {
console.log("ParentComponent 렌더링");
});
return (
<>
<ContextProvider value={"hello"}>
<input value={text} onChange={handleChangeText} />
<ChildComponent />
</ContextProvider>
</>
);
}
```
이렇게 Parent에서 내려주는 Context를 GrandChild가 사용하고 있을 때 언뜻 보기에는 Parent와 GrandChild만 리렌더링 될 것 같지만 사실은 컴포넌트 트리 전체가 리렌더링되고 있다.
![[616e8627-e7a2-4ea3-9d3f-45ae1bea9893.png]]
>[!tips] 이거 왜 이런거지?
> 참고 - [[2.4 렌더링은 어떻게 일어나는가?]]
> Context 때문이 아니라 **일반적인 React 렌더링 규칙 때문**이다.
> ParentComponent가 리렌더링되면
> **부모의 자식이므로** ChildComponent 리렌더링
> **부모의 자식이므로** GrandChildCompoent 리렌더링
그래서 이 예제를 최적화해서 관련 없는 ChildComponent가 리렌더링되지 않게 하기 위해서는 React.memo를 사용하면 된다. memo는 props 변화가 없으면 리렌더링되지 않고 같은 결과값을 반환한다.
```tsx
const ChildComponent = memo(() => {
useEffect(() => {
console.log("ChildComponent 렌더링");
});
return <GrandChildComponent />;
});
```
![[8a23af9d-8558-4a15-9701-b18a4a7501e6.png]]
이처럼 useContext로 상태 주입을 최적화했다면 반드시 Provider로 값이 변경될 때 어떤 식으로 렌더링되는지 유심히 봐야 한다. useContext는 주입된 상태를 사용할 수 있을 뿐 그 자체로는 렌더링 최적화에 아무런 도움이 되지 않는다.
## useReducer
useReducer는 [[#useState]]의 심화 버전으로 볼 수 있다.
useState와 비슷한 형태를 띠지만 좀 더 복잡한 상태값을 미리 정의해 놓은 시나리오에 따라 관리할 수 있다.
**반환값은 useState와 동일하게 길이가 2인 배열이다.**
```js
const [state, dispatcher] = useReducer();
```
- state는 현재 useReducer가 갖고 있는 값을 의미한다.
- dispatcher는 state를 업데이트하는 함수이다. useState의 setState에서는 단순히 값을 전달해서 업데이트하지만 dispatcher에는 action을 넘겨준다는 점이 다르다.
**useState와는 다르게 2개에서 3개의 인수를 필요로 한다**
```js
const [state, dispatcher] = useReducer(reducer, initialState, init);
```
- reducer : useReducer의 기본 action을 정의하는 함수이다.
- initialState : useReducer의 초깃값을 의미한다.
- init : useState의 인수로 함수를 넘겨줄 때 처럼 지연 초기화를 하고 싶을 때 사용하는 함수이다. 만약 3번째 인수를 전달한다면 useState와 동일하게 게으른 초기화가 일어나며 initialState를 인수로 init 함수가 실행된다.
```tsx
type State = {
count: number;
};
// state의 변화를 발생시킬 action의 타입과 넘겨줄 값(payload)의 타입을 정의한다.
// ⭐️ type, payload라는 이름이 관례적으로 쓰인다.
type Action = { type: "increase" | "decrease" | "reset"; payload?: State };
// 게으른 초기화용
function init(count: State): State {
// 무언가 무거운 작업...
return count;
}
// 초깃값
const initialState: State = { count: 0 };
// 정의한 action에 따라서 state를 어떻게 변화시킬지 정의한다.
// ⭐️ reducer는 순수 함수여야 하기 때문에 직접 값을 변화시키는 것이 아닌 객체를 반환한다.
function reducer(state: State, action: Action): State {
switch (action.type) {
case "increase":
return { count: state.count + 1 };
case "decrease":
return { count: state.count - 1 };
case "reset":
return init(action.payload!);
default:
throw new Error("Unhandled action");
}
}
function Component() {
const [state, dispatcher] = useReducer(reducer, initialState, init);
function increase() {
dispatcher({ type: "increase" });
}
function decrease() {
dispatcher({ type: "decrease" });
}
function reset() {
dispatcher({ type: "reset", payload: initialState });
}
return (
<>
<div>Count: {state.count}</div>
<button onClick={increase}>Increase</button>
<button onClick={decrease}>Decrease</button>
<button onClick={reset}>Reset</button>
</>
);
}
```
사용 방법이 좀 복잡해 보이긴 하지만 useReducer는 state 값을 변경하는 시나리오를 제한적으로 두고 이에 대한 변경을 컴포넌트에서 빠르게 확인할 수 있게끔 한다는 간단한 목적을 갖고 있다.
일반적으로 단순히 number나 boolean 같이 간단한 값을 관리하는 것은 useState 하나만으로 간단하지만 state 하나가 가져야 할 값이 복잡하고 이를 수정하는 경우의 수가 많아진다면 state를 관리하는 것이 어려워진다. 또 여러개의 state를 나눠서 관리하는 것 보다 하나의 useReducer로 관리하는 것이 더 편하고 효율적일수도 있다.
앞서서 간단히 살펴보았듯 [[#useState]]는 useReducer로 구현되어 있다.
[preact/hooks/src/index.js at main · preactjs/preact · GitHub](https://github.com/preactjs/preact/blob/main/hooks/src/index.js#L172)
```js
export function useState(initialState) {
currentHook = 1;
return useReducer(invokeOrReturn, initialState);
}
```
그럼 직접 useState를 useReducer로 구현해보자.
```js
function reducer(prevState, newState) {
// ⭐️ newState가 함수면 실행 (함수형 업데이트)
// ⭐️ 아니면 그대로 반환 (직접 값 전달)
return typeof newState === "function" ? newState(prevState) : newState;
}
function init(initialArg) {
// ⭐️ 게으른 초기화
return typeof initialArg === "function" ? initialArg() : initialArg;
}
function useState(initialArg) {
return useReducer(reducer, initialArg, init);
}
```
이와 반대로 useReducer를 useState로 구현할 수도 있다.
```jsx
function useReducer(reducer, initialArg, init) {
const [state, setState] = useState(
// init이 존재하면 게으른 초기화 함수를 실행하고
// 없으면 initialArg를 상태의 초기값으로 사용한다.
init ? init(initialArg) : initialArg
);
// 값을 업데이트하는 dispatch 함수를 만든다.
const dispatch = useCallback(
(action) => setState((prevState) => reducer(prevState, action)),
[reducer]
);
return useMemo(() => [state, dispatch], [state, dispatch]);
}
```
즉 useReducer나 useState 둘 다 세부 작동과 쓰임에만 차이가 있을 뿐, 결국 클로저를 이용해서 값을 가둬서 state를 관리한다는 사실에는 변함이 없다. 필요에 따라 useReducer나 useState를 잘 선택해서 사용하면 되는 것이다.
## useImperativeHandle
### forwardRef
useImperativeHandle을 이해하기 위해서는 React.forwardRef에 대해 알아야 한다.
ref는 useRef에서 반환한 객체이다. key와 마찬가지로 ref도 리액트에서 컴포넌트의 props로 사용할 수 있는 예약어이다. 만약 이러한 ref를 상위 컴포넌트에서 하위 컴포넌트로 전달하고 싶다면 어떻게 해야 할까?
```jsx
function ChildComponent({ ref }) {
useEffect(() => {
// undefined
console.log(ref);
}, [ref]);
return <div>안녕!</div>;
}
function Component() {
const inputRef = useRef();
return (
<>
<input type="text" ref={inputRef} />
{/* ref is not a prop. Trying to access it will result in undefined being returned. If you need to access the same value within the child component, you should pass it as a different prop */}
<ChildComponent ref={inputRef} />
</>
);
}
```
> [!tip]- React 19부터는 더 이상 forwardRef가 필요하지 않다.
> [React v19 – React](https://react.dev/blog/2024/12/05/react-19#ref-as-a-prop)
> 위 코드를 React 19에서 돌려보면 에러가 발생하지 않는다. 공식 문서에 따르면 forwardRef도 제거될 예정이라고 한다.
> > New function components will no longer need `forwardRef`, and we will be publishing a codemod to automatically update your components to use the new `ref` prop. In future versions we will deprecate and remove `forwardRef`.
리액트에서 ref는 props로 쓸 수 없다는 경고문이 뜬다. 예약어로 지정된 다른 props 이름으로 보내면 경고 없이 잘 동작한다.
```jsx
function ChildComponent({ parentRef }) {
useEffect(() => {
console.log(parentRef);
}, [parentRef]);
return <div>안녕!</div>;
}
function Component() {
const inputRef = useRef();
return (
<>
<input type="text" ref={inputRef} />
<ChildComponent parentRef={inputRef} />
</>
);
}
```
forwardRef는 이렇게 ref를 props로 전달해주는 리액트 API이다. 단순히 props로 전달해도 되는 작업을 따로 API를 만든 이유는 ref를 전달하는 데 있어서 일관성을 제공하기 위해서이다. 어떤 props로 전달해도 괜찮기 때문에 완전한 네이밍의 자유가 주어진 단순 props보다는 forwardRef를 이용해서 일관되게 보내는 것이 더 안정적이기 때문이다.
```jsx
const ChildComponent = forwardRef((props, ref) => {
useEffect(() => {
// undefined
console.log(ref);
}, [ref]);
return <div>안녕!</div>;
});
function Component() {
const inputRef = useRef();
return (
<>
<input type="text" ref={inputRef} />
<ChildComponent ref={inputRef} />
</>
);
}
```
이렇게 ref를 받을 컴포넌트를 forwardRef로 감싸고, ref를 전달하는 쪽에서는 동일하게 props.ref로 ref를 넘겨주면 된다.
### useImperativeHandle이란
useImperativeHandle이란 부모에게서 넘겨받은 ref를 원하는 대로 수정할 수 있는 훅이다.
```jsx
const Input = forwardRef((props, ref) => {
// useImperativeHandle을 사용하면 ref의 동작을 추가로 정의할 수 있다.
useImperativeHandle(
ref,
() => ({
alert: () => alert(props.value),
}),
[props.value] // useEffect의 의존성 배열과 동일
);
return <input type="text" ref={ref} {...props} />;
});
Input.displayName = "Input";
function Component() {
const inputRef = useRef();
const [text, setText] = useState("");
function handleClick() {
// inputRef에 추가한 alert 메서드를 호출할 수 있다.
inputRef.current.alert();
}
function handleChange(e) {
setText(e.target.value);
}
return (
<>
<Input type="text" ref={inputRef} value={text} onChange={handleChange} />
<button onClick={handleClick}>Alert Input Value</button>
</>
);
}
```
원래 ref는 `{current: <HTMLElement>}` 와 같은 형태로 HTMLElement만 주입할 수 있는 객체였다. 그런데 여기서는 전달받은 ref에다 useImperativeHandle 훅을 이용해 추가적인 동작을 정의했다. 이로서 부모는 단순히 HTMLElement 뿐 아니라 자식 컴포넌트에서 새롭게 설정한 객체의 키와 값에 대해서도 접근할 수 있게 되었다.
> [!tip]- 이게 어디에 쓰이는거지?
> **사용 사례 1: 복잡한 컴포넌트의 일부 메서드만 노출**
> ```tsx
> const VideoPlayer = forwardRef((props, ref) => {
> const videoRef = useRef();
>
> useImperativeHandle(ref, () => ({
> // 부모에게 필요한 메서드만 노출
> play: () => videoRef.current.play(),
> pause: () => videoRef.current.pause(),
> seek: (time) => videoRef.current.currentTime = time,
> // video 태그의 모든 메서드를 노출하지 않고 필요한 것만 선택
> }));
>
> return <video ref={videoRef} {...props} />;
> });
> ```
>
> **사용 사례 2: 라이브러리/디자인 시스템 컴포넌트**
>
> 사용 사례긴 한데, React 공식 문서에서는 Modal 같은 사례를 Prop으로 표현할 수 있는 것이라고 설명했고, 이런 것에는 Ref를 사용하지 말라고 설명하고 있긴 하다.
> [useImperativeHandle – React](https://ko.react.dev/reference/react/useImperativeHandle)
>
> ```tsx
> const Dialog = forwardRef((props, ref) => {
> const [isOpen, setIsOpen] = useState(false);
>
> useImperativeHandle(ref, () => ({
> open: () => setIsOpen(true),
> close: () => setIsOpen(false),
> // 내부 state를 직접 노출하지 않고 메서드로 제어
> }));
>
> return isOpen ? <div>{props.children}</div> : null;
> });
>
> // 사용
> function App() {
> const dialogRef = useRef();
>
> return (
> <>
> <button onClick={() => dialogRef.current.open()}>
> Open Dialog
> </button>
> <Dialog ref={dialogRef}>Dialog Content</Dialog>
> </>
> );
> }
> ```
## useLayoutEffect
[useLayoutEffect – React](https://ko.react.dev/reference/react/useLayoutEffect)
공식 문서에 따르면 useLayoutEffect는 함수의 시그니쳐는 useEffect와 동일하지만 모든 DOM의 변경 후에 동기적으로 발생하는 훅이다.
먼저 함수의 시그니쳐가 useEffect와 동일하다는 것은 사용법이 useEffect와 동일하다는 것을 의미한다.
```jsx
useEffect(() => {
console.log("useEffect", count);
}, [count]);
useLayoutEffect(() => {
console.log("useLayoutEffect", count);
}, [count]);
```
중요한 부분은 **'모든 DOM의 변경 후에 useLayoutEffect의 콜백 함수 실행이 동기적으로 발생'** 한다는 점이다. 여기서 DOM의 변경이란 React가 실제 DOM에 변경 사항을 commit한 것을 의미한다. 하지만 브라우저가 화면에 그리는 paint 시점은 아니다. 즉, DOM은 업데이트되었지만 사용자는 아질 볼 수 없는 상태이다.
1. 리액트가 DOM을 업데이트
2. useLayoutEffect를 실행
3. 브라우저에 변경 사항을 반영
4. useEffect를 실행
그리고 동기적으로 발생한다는 것은 리액트는 useLayoutEffect의 실행이 종료되기 전까지는 화면을 그리지 않는다는 것을 의미한다. 따라서 이러한 작동 방식으로 인해서 useLayoutEffect에 무거운 작업이 있다면 웹 어플리케이션 성능에 문제가 생길 수 있다.
useLayoutEffect의 특징상 DOM은 계산되었지만 그것이 화면에 반영되기 전에 하고 싶은 작업이 있을 때 사용하면 좋다. 예를 들면 DOM 요소를 기반으로 한 애니메이션이나 스크롤 위치를 제어하는 등 화면에 반영되기 전에 하고 싶은 작업에 useLayoutEffect를 사용하면 useEffect를 사용할 때 보다 더 자연스러운 사용자 경험을 제공할 수 있다.
```jsx
// useEffect 사용 시 - 깜빡임 발생 가능
function Tooltip() {
const [position, setPosition] = useState({ x: 0, y: 0 });
const ref = useRef(null);
useEffect(() => {
// paint 후에 실행되므로
// 사용자가 잠깐 잘못된 위치를 볼 수 있음
if (ref.current) {
const rect = ref.current.getBoundingClientRect();
setPosition({ x: rect.left, y: rect.top });
}
}, []);
return <div ref={ref}>Tooltip</div>;
}
// useLayoutEffect 사용 시 - 깜빡임 없음
function Tooltip() {
const [position, setPosition] = useState({ x: 0, y: 0 });
const ref = useRef(null);
useLayoutEffect(() => {
// paint 전에 실행되므로
// 사용자는 올바른 위치만 봄
if (ref.current) {
const rect = ref.current.getBoundingClientRect();
setPosition({ x: rect.left, y: rect.top });
}
}, []);
return <div ref={ref}>Tooltip</div>;
}
```
## useDebugValue
useDebugValue 훅은 프로덕션 레벨에서 사용하는 훅은 아니고, 개발 과정에서 사용하는 훅이다. 디버깅하고 싶은 정보와 함께 사용하면 리액트 개발자 도구에서 확인할 수 있다.
```jsx
// 현재 시간을 반환하는 사용자 정의 훅
function useDate() {
const date = new Date();
// useDebugValue를 이용해서 디버깅 정보를 기록한다.
useDebugValue(date, (date) => date.toISOString());
return date;
}
function Component() {
const date = useDate();
const [count, setCount] = useState(0);
function handleClick() {
setCount((prevCount) => prevCount + 1);
}
return (
<>
<p>현재 시간: {date.toLocaleTimeString()}</p>
<p>클릭 횟수: {count}</p>
<button onClick={handleClick}>클릭</button>
</>
);
}
```
![[60ecbd9f-341a-4e50-a02c-d2667ec0c3a6.png]]
이렇게 리액트 개발자 도구에서 확인할 수 있다.
useDebugValue는 사용자 정의 훅 내부의 내용에 대한 정보를 남길 수 있는 훅이다. 두번째 인수로 포매팅 함수를 전달하면 첫번째 인수의 값이 변경될 때만 호출되어 포매팅 된 값을 출력한다.
useDebugValue 훅은 오직 다른 훅 내부에서만 실행할 수 있고 컴포넌트 레벨에서 사용할 때에는 동작하지 않을 것이다. 공유 라이브러리나 복잡한 로직을 사용하고 있는 커스텀 훅에서 내부 상태를 확인할 때에 유용하게 사용할 수 있다.
## 훅의 규칙
훅을 사용하는 데는 몇가지 규칙이 존재한다. 이러한 규칙을 rules-of-hooks라고 하고 이와 관련된 ESLint 규칙인 react-hooks/rules-of-hooks도 있다.
[Hook의 규칙 – React](https://ko.legacy.reactjs.org/docs/hooks-rules.html)
- 반복문, 조건문, 중첩된 함수 내에서 훅을 실행할 수 없다. 최상위에서만 훅을 실행해서 항상 동일한 순서로 훅이 호출되는 것을 보장해야 한다. (react 19에서 새로 등장한 [use 훅](https://react.dev/blog/2024/12/05/react-19#new-feature-use)은 조건부로 사용할 수 있다.)
- 훅을 호출할 수 있는 것은 함수 컴포넌트 또는 사용자 정의 훅의 두가지 경우 뿐이다.
---
[[#useState]]나 [[#useEffect의 구현|useEffect]]의 구현에서 볼 수 있듯이 훅에 대한 정보는 index같은 key를 이용해서 관리되고 잇다. 그렇기 때문에 useState나 useEffect는 호출 순서에 아주 큰 영향을 받는다.
```jsx
function Component() {
const [count, setCount] = useState(0);
const [required, setRequired] = useState(false);
useEffect(() => {
// do something...
}, [count, required]);
return <></>;
}
```
이 컴포넌트는 파이버에서 아래와 같이 저장된다.
> [!tip]- 파이버 객체 내부 확인하는 법
> ```js
> // 콘솔에서 실행
> const el = document.getElementById('my-component');
> const key = Object.keys(el).find(k => k.startsWith('__reactFiber