Skip to content

React Context 기반의 에러 UI 렌더링 라이브러리입니다. Provider가 에러 상태를 관리하고, 사용자가 제공한 커스텀 렌더러로 에러 UI를 표시합니다.

설치

bash
pnpm add @sanghyuk-2i/huh-core @sanghyuk-2i/huh-react
bash
npm install @sanghyuk-2i/huh-core @sanghyuk-2i/huh-react
bash
yarn add @sanghyuk-2i/huh-core @sanghyuk-2i/huh-react

Peer Dependencies: react >= 18, react-dom >= 18


HuhProvider

에러 상태를 관리하고, 활성 에러가 있을 때 해당 타입의 렌더러를 호출합니다.

Props

ts
interface HuhProviderProps {
  source?: ErrorConfig; // JSON DSL 데이터 (단일 언어 모드)
  locales?: LocalizedErrorConfig; // 다국어 에러 설정 (i18n 모드)
  defaultLocale?: string; // 기본 로케일
  locale?: string; // 현재 로케일 (외부 제어)
  renderers: RendererMap; // 커스텀 렌더러 (필수)
  children: ReactNode;
  onRetry?: () => void; // RETRY 액션 시 호출되는 콜백
  onCustomAction?: (action: { type: string; target?: string }) => void; // 커스텀 액션 콜백
  plugins?: HuhPlugin[]; // 모니터링/분석용 플러그인 배열
  errorMap?: Record<string, string>; // 에러 코드→trackId 매핑 테이블
  fallbackTrackId?: string; // 매핑이 없을 때 사용할 기본 trackId
  router?: HuhRouter; // 클라이언트 사이드 라우팅용 커스텀 라우터 (예: Next.js useRouter)
}

TIP

sourcelocales 중 하나를 제공해야 합니다. source는 기존 단일 언어 모드, locales는 다국어 모드입니다.

기본 사용법

tsx
import errorContent from './huh.json';
import { HuhProvider } from '@sanghyuk-2i/huh-react';

function App() {
  return (
    <HuhProvider
      source={errorContent}
      renderers={renderers}
      onRetry={() => window.location.reload()}
      onCustomAction={(action) => {
        // 커스텀 액션 타입 처리 (예: OPEN_CHAT, SHARE 등)
        if (action.type === 'OPEN_CHAT') openChatWidget();
      }}
    >
      <YourApp />
    </HuhProvider>
  );
}

라우터 연동

router prop을 전달하면 full page reload 대신 프레임워크별 클라이언트 사이드 네비게이션을 사용합니다:

tsx
// Next.js
import { useRouter } from 'next/navigation';

function App() {
  const router = useRouter();
  return (
    <HuhProvider
      source={errorContent}
      renderers={renderers}
      router={{ push: router.push, back: router.back }}
    >
      <YourApp />
    </HuhProvider>
  );
}

router가 제공되면 REDIRECT 액션은 router.push()를, BACK 액션은 router.back()을 사용합니다. 미제공 시 기존 window.location.hrefwindow.history.back() 동작이 유지됩니다.


RendererMap

에러 타입별 렌더러를 제공합니다. 기본 렌더러는 없으며, 에러 발생 시 해당 타입의 렌더러가 없으면 런타임 에러가 발생합니다. 키는 대문자 타입명입니다.

ts
type RendererMap = Record<string, (props: ErrorRenderProps) => ReactNode>;

기본 제공 타입(TOAST, MODAL, PAGE) 외에도 커스텀 타입에 대한 렌더러를 자유롭게 추가할 수 있습니다:

ts
  TOAST: ({ error, onDismiss }) => <Toast message={error.message} onClose={onDismiss} />,
  MODAL: ({ error, onAction, onDismiss }) => <Modal ... />,
  PAGE: ({ error, onAction }) => <ErrorPage ... />,
  // 커스텀 타입 렌더러
  BANNER: ({ error, onAction, onDismiss }) => <Banner message={error.message} ... />,
  SNACKBAR: ({ error, onDismiss }) => <Snackbar message={error.message} ... />,
};

ErrorRenderProps

각 렌더러에 전달되는 props입니다.

ts
interface ErrorRenderProps {
  error: ResolvedError; // 변수 치환이 완료된 에러 정보
  onAction: () => void; // 액션 버튼 클릭 핸들러
  onDismiss: () => void; // 닫기 핸들러
}
  • error.type'TOAST' | 'MODAL' | 'PAGE' | string (커스텀 타입 포함)
  • error.message — 치환 완료된 메시지
  • error.title — 치환 완료된 제목 (선택)
  • error.image — 이미지 URL (선택)
  • error.action — 액션 정보 (선택)

onAction 동작

onAction은 에러의 action.type에 따라 자동으로 동작합니다:

actionType동작
REDIRECTrouter 제공 시 router.push(target), 미제공 시 window.location.href = target
BACKrouter 제공 시 router.back(), 미제공 시 window.history.back()
RETRY에러 클리어 + onRetry 콜백 호출
DISMISS에러 클리어
커스텀 타입에러 클리어 + onCustomAction 콜백 호출
액션 없음에러 클리어

커스텀 액션 타입(예: OPEN_CHAT, SHARE)은 onCustomAction 콜백에 { type, target } 객체가 전달됩니다.

렌더러 구현 예시

tsx
import type { RendererMap } from '@sanghyuk-2i/huh-react';
import { Toast } from '@/components/Toast';
import { Modal } from '@/components/Modal';

const renderers: RendererMap = {
  TOAST: ({ error, onDismiss }) => <Toast message={error.message} onClose={onDismiss} />,

  MODAL: ({ error, onAction, onDismiss }) => (
    <Modal open onClose={onDismiss}>
      <Modal.Title>{error.title}</Modal.Title>
      <Modal.Body>{error.message}</Modal.Body>
      <Modal.Footer>
        {error.action && <button onClick={onAction}>{error.action.label}</button>}
        <button onClick={onDismiss}>닫기</button>
      </Modal.Footer>
    </Modal>
  ),

  PAGE: ({ error, onAction }) => (
    <div className="flex flex-col items-center justify-center min-h-screen">
      {error.image && <img src={error.image} alt="" className="w-48 mb-8" />}
      <h1 className="text-3xl font-bold">{error.title}</h1>
      <p className="mt-4 text-gray-600">{error.message}</p>
      {error.action && (
        <button onClick={onAction} className="mt-8 btn btn-primary">
          {error.action.label}
        </button>
      )}
    </div>
  ),

  // 커스텀 타입 예시
  BANNER: ({ error, onAction, onDismiss }) => (
    <div className="bg-yellow-100 border-l-4 border-yellow-500 p-4">
      <p>{error.message}</p>
      {error.action && <button onClick={onAction}>{error.action.label}</button>}
      <button onClick={onDismiss}>닫기</button>
    </div>
  ),
};

useHuh

Provider 하위에서 에러를 트리거하거나 클리어하는 훅입니다.

ts

interface HuhContextValue {
  huh: (code: string, variables?: Record<string, string>) => void;
  clearError: () => void;
  locale: string | undefined; // 현재 로케일 (i18n 모드)
  setLocale: (locale: string) => void; // 로케일 변경 (i18n 모드)
}

WARNING

Provider 밖에서 호출하면 에러가 발생합니다.

huh(code, variables?)

에러를 트리거하는 단일 함수입니다. trackId 직접 지정, errorMap 매핑, fallback을 모두 처리합니다.

조회 순서:

  1. errorMap에서 코드 매핑 확인
  2. 코드가 직접 trackId와 일치하는지 확인
  3. fallbackTrackId 사용
  4. 매핑이 없으면 에러 throw
tsx
import { huh } = useHuh();

// trackId로 직접 에러 트리거
huh('ERR_NETWORK');

// 변수 치환과 함께 트리거
huh('ERR_SESSION_EXPIRED', { userName: '홍길동' });

// API 에러 코드를 errorMap으로 매핑
try {
  await api.call();
} catch (e) {
  huh(e.code); // 'API_500' → errorMap → 'ERR_SERVER'
}

errorMap 설정:

tsx
<HuhProvider
  source={config}
  renderers={renderers}
  errorMap={{ API_500: 'ERR_SERVER', API_401: 'ERR_AUTH' }}
  fallbackTrackId="ERR_UNKNOWN"
>
  <App />
</HuhProvider>

clearError()

현재 활성화된 에러 UI를 닫습니다.

tsx
import { clearError } = useHuh();

clearError();

전체 예시

tsx
import React from 'react';
import errorContent from './huh.json';
import { HuhProvider, useHuh } from '@sanghyuk-2i/huh-react';
import type { RendererMap } from '@sanghyuk-2i/huh-react';

const renderers: RendererMap = {
  TOAST: ({ error, onDismiss }) => (
    <div className="fixed bottom-4 right-4 bg-red-500 text-white p-4 rounded">
      {error.message}
      <button onClick={onDismiss} className="ml-2">
        X
      </button>
    </div>
  ),
  MODAL: ({ error, onAction, onDismiss }) => (
    <div className="fixed inset-0 bg-black/50 flex items-center justify-center">
      <div className="bg-white p-6 rounded-lg">
        <h2 className="text-xl font-bold">{error.title}</h2>
        <p className="mt-2">{error.message}</p>
        <div className="mt-4 flex gap-2">
          {error.action && (
            <button onClick={onAction} className="btn-primary">
              {error.action.label}
            </button>
          )}
          <button onClick={onDismiss}>닫기</button>
        </div>
      </div>
    </div>
  ),
  PAGE: ({ error, onAction }) => (
    <div className="min-h-screen flex flex-col items-center justify-center">
      <h1 className="text-4xl">{error.title}</h1>
      <p className="mt-4">{error.message}</p>
      {error.action && (
        <button onClick={onAction} className="mt-8 btn-primary">
          {error.action.label}
        </button>
      )}
    </div>
  ),
};

function UserProfile() {
  const { huh } = useHuh();

  const loadProfile = async () => {
    try {
      const res = await fetch('/api/profile');
      if (!res.ok) throw new Error();
    } catch {
      huh('ERR_PROFILE_LOAD');
    }
  };

  return <button onClick={loadProfile}>프로필 로드</button>;
}

export default function App() {
  return (
    <HuhProvider
      source={errorContent}
      renderers={renderers}
      onRetry={() => console.log('Retrying...')}
    >
      <UserProfile />
    </HuhProvider>
  );
}

Released under the MIT License.