use 훅이란?
Suspense란?
<Suspense>
lets you display a fallback until its children have finished loading.
위의 문장에서 알 수 있듯이 <Suspense>
란 자식 요소가 로딩을 마칠 때까지 대체 UI를 표시할 수 있도록 해주는 컴포넌트입니다.
그렇다면 어떻게 자식 요소가 로딩 중인지, 로딩이 끝났는지를 알 수 있을까요?
리액트 문서에서는 아래와 같은 경우에 <Suspense>
를 활용할 수 있다고 명시합니다.
- NextJs와 같은 Suspense를 지원하는 프레임워크에서 데이터를 가져오는 경우
lazy
를 이용한 컴포넌트 지연 로딩의 경우use
훅을 이용한 Promise 값 읽기의 경우
여기서 1, 2번은 많은 사례와 경험이 존재합니다.
하지만 3번의 경우에는 조금 낯선 개념입니다.
왜냐하면 use
훅의 경우 리액트 19에서 정식 도입된(될) 기능이기 때문입니다.
use 훅에 대해서
use
훅을 살펴보기 전에 웹 애플리케이션에서 일반적으로 데이터를 패칭하는 경우를 생각해봅시다.
일반적으로 웹 애플리케이션에서 데이터를 페칭할 때 fetch API를 사용합니다. fetch를 사용하면 응답을 스트리밍하거나 JSON 형태의 응답을 받을 수 있습니다.
이러한 일반적인 데이터 패칭 과정에 아래와 같이 고려해야할 것들이 존재합니다
-
로딩 상태 처리
- 데이터 페칭 시작부터 완료까지는 시간이 소요됨 (데이터 크기에 따라 상이)
- 이 시간 동안의 시각적 피드백이 필요함
- 빈 화면이나 변화 없는 화면은 사용자 경험을 저하시킴
- 네트워크 상태나 서버 응답 속도는 제어할 수 없으므로 항상 고려해야 함
-
에러 상태 처리
- 성공적인 응답(2xx) 외의 상황 처리 필요
- 클라이언트 에러(4xx)나 서버 에러(5xx) 발생 가능
- 네트워크 오류 등 다양한 에러 상황 대비
이러한 처리를 도와주는 것이 use
훅입니다.
function PhoneDetails() {
const details = use(phoneDetailsPromise);
// details 사용
}
비동기 처리 전
async function UserProfile() { const user = await fetchUserProfile(); return <div>{user.name}</div>; }
use
훅의 사용법은 위처럼 간단합니다. use(Promise 데이터)
하지만 일반적인 데이터 패칭과 다른점이 존재합니다.
비동기 데이터를 다루는데 await
등의 비동기 처리방식이 존재하지 않습니다. 그렇다면 use
훅은 어떻게 비동기 데이터를 처리하는걸까요?
use
훅은 내부적으로 아래와 같은 과정을 거칩니다
내부 동작 메커니즘
- React가 컴포넌트를 try-catch로 감싸서 처리
- 프로미스 상태에 따른 처리
- resolve된 경우: 결과값 반환
- reject된 경우: 에러 throw
- pending 상태: 프로미스 자체를 throw
- throwing 메커니즘을 통한 실행 중단 처리
글로만 보면 이해가 조금 어려우니 예시를 들어보겠습니다.
use훅 직접 구현해보기
use 훅의 동작 원리를 이해하기 위해 간단한 버전을 직접 구현해보겠습니다. 예시로 사용자 프로필을 불러오는 상황을 살펴보겠습니다.
1. 초기 상태
일반적으로 우리가 작성하는 비동기 데이터 로딩 코드입니다
// UserProfile.tsx
async function UserProfile() {
const user = await fetchUserProfile("user-1"); // 2초 지연
return (
<div>
<h2>{user.name}의 프로필</h2>
<UserDetails user={user} />
</div>
);
}
이 코드의 문제점
- 데이터 로딩 중에는 빈 화면 표시
- 로딩 상태 표시 없음
- 에러 처리 부재
2. use 훅 구현하기
먼저 기본적인 타입을 정의합니다
type UsePromise<T> = Promise<T> & {
status: 'pending' | 'fulfilled' | 'rejected';
value?: T;
reason?: unknown;
};
function use<T>(promise: Promise<T>): T {
const usePromise = promise as UsePromise<T>;
// 초기 상태 설정
if (!('status' in usePromise)) {
usePromise.status = 'pending';
// Promise 처리 설정
usePromise.then(
(value) => {
usePromise.status = 'fulfilled';
usePromise.value = value;
},
(reason) => {
usePromise.status = 'rejected';
usePromise.reason = reason;
},
);
}
// 상태에 따른 처리
switch (usePromise.status) {
case 'fulfilled':
return usePromise.value!;
case 'rejected':
throw usePromise.reason;
case 'pending':
default:
throw usePromise;
}
}
3. use 훅을 활용한 개선된 코드
import { Suspense } from 'react';
import { ErrorBoundary } from 'react-error-boundary';
function UserProfile() {
const userPromise = fetchUserProfile("user-1");
const user = use(userPromise); // use 훅 사용
return (
<div>
<h2>{user.name}의 프로필</h2>
<UserDetails user={user} />
</div>
);
}
// 사용 예시
function App() {
return (
<ErrorBoundary
fallback={<div>사용자 정보를 불러오는데 실패했습니다</div>}
>
<Suspense fallback={<div>프로필 불러오는 중...</div>}>
<UserProfile />
</Suspense>
</ErrorBoundary>
);
}
4. 동작 방식 설명
-
초기 렌더링
userPromise
생성use
훅이 Promise 상태 확인- pending 상태이므로 Promise throw
- Suspense가 catch하여 fallback UI 표시
-
Promise 해결됨
- Promise가 resolve되면 status가 'fulfilled'로 변경
- value에 결과값 저장
- React가 컴포넌트 재렌더링
- use 훅이 저장된 value 반환
- UserProfile 컴포넌트 정상 렌더링
-
에러 발생 시
- Promise가 reject되면 status가 'rejected'로 변경
- reason에 에러 저장
- use 훅이 에러 throw
- ErrorBoundary가 catch하여 에러 UI 표시
5. 타입스크립트로 더 안전하게
에러 처리를 위한 커스텀 타입을 추가해봅시다
type UserError = {
code: string;
message: string;
status: number;
};
type UsePromiseState<T> =
| {
status: 'pending';
value?: undefined;
reason?: undefined;
}
| {
status: 'fulfilled';
value: T;
reason?: undefined;
}
| {
status: 'rejected';
value?: undefined;
reason: UserError;
};
type UsePromise<T> = Promise<T> & UsePromiseState<T>;
이렇게 타입을 정의하면 각 상태별로 어떤 데이터가 있어야 하는지 명확해집니다.
장점과 의의
-
선언적 코드
- 복잡한 상태 관리가 훅 하나로 축소
- 비동기 처리 로직이 컴포넌트에서 분리
-
타입 안정성
- 제네릭을 통한 반환값 타입 보장
- 각 상태별 데이터 존재 여부를 타입으로 검증
-
사용성
- 간단한 API로 복잡한 비동기 처리
- React의 Suspense, ErrorBoundary와 자연스러운 통합
-
유지보수성
- 상태 처리 로직 중앙화
- 명확한 에러 처리 흐름
선언적 코드
복잡한 상태 관리를 단순화
타입 안정성
제네릭을 통한 타입 보장
자동 상태 관리
로딩/에러 상태 자동 처리
에러 핸들링
선언적 에러 처리 지원
React 19에서 정식으로 도입될 use 훅은 비동기 데이터 처리에 있어 새로운 패러다임을 제시합니다. 위 구현은 실제 React의 구현체와는 차이가 있지만, 기본적인 개념을 이해하는 데 도움이 될 것입니다.
Promise 라이프사이클 데모
await getData()