huh Svelte Themes

Huh

πŸ€” Error content as data: Manage error messages in spreadsheets (Google Sheets, Airtable, Notion) with type-safe runtime UI rendering


Huh (μ—₯?)

μ—λŸ¬ λ©”μ‹œμ§€λŠ” μ½”λ“œκ°€ μ•„λ‹ˆλΌ μŠ€ν”„λ ˆλ“œμ‹œνŠΈμ—μ„œ κ΄€λ¦¬ν•˜μ„Έμš”.

λΉ„κ°œλ°œμžλŠ” Google Sheets, Airtable, Notion λ“±μ—μ„œ μ—λŸ¬ μ½˜ν…μΈ λ₯Ό κ΄€λ¦¬ν•˜κ³ , κ°œλ°œμžλŠ” νƒ€μž… μ•ˆμ „ν•œ μ—λŸ¬ UIλ₯Ό μžλ™μœΌλ‘œ λ Œλ”λ§ν•©λ‹ˆλ‹€.

μ‹œμž‘ν•˜κΈ° | API 레퍼런슀 | μ•„ν‚€ν…μ²˜

English / μ˜μ–΄


이런 문제λ₯Ό κ²ͺκ³  μžˆμ§€ μ•Šλ‚˜μš”?

λͺ¨λ“  ν”„λ‘ νŠΈμ—”λ“œ νŒ€μ€ κ²°κ΅­ μ—λŸ¬ λ©”μ‹œμ§€κ°€ μ½”λ“œλ² μ΄μŠ€ 여기저기에 ν©μ–΄μ§€λŠ” 문제λ₯Ό κ²ͺμŠ΅λ‹ˆλ‹€. κΈ°νšμžκ°€ 문ꡬ 변경을 μš”μ²­ν•˜λ©΄ 티켓을 λ§Œλ“€κ³ , κ°œλ°œμžκ°€ μ½”λ“œλ₯Ό μˆ˜μ •ν•˜κ³ , λ°°ν¬κΉŒμ§€ κΈ°λ‹€λ €μ•Ό ν•©λ‹ˆλ‹€. μ—λŸ¬ 처리 λ‘œμ§μ€ μ»΄ν¬λ„ŒνŠΈλ§ˆλ‹€ μ œκ°κ°μž…λ‹ˆλ‹€. μ§„μ‹€μ˜ μ›μ²œ(Single Source of Truth)이 μ—†μŠ΅λ‹ˆλ‹€.

HuhλŠ” μ™ΈλΆ€ 데이터 μ†ŒμŠ€λ₯Ό μ—λŸ¬ μ½˜ν…μΈ μ˜ 단일 μ§„μ‹€ μ›μ²œμœΌλ‘œ λ§Œλ“€μ–΄ 이 문제λ₯Ό ν•΄κ²°ν•©λ‹ˆλ‹€:

데이터 μ†ŒμŠ€ (κΈ°νšμžκ°€ μˆ˜μ •) β†’ huh pull β†’ huh.json β†’ λŸ°νƒ€μž„ UI

Google Sheets, Airtable, Notion, CSV, XLSXλ₯Ό μ§€μ›ν•©λ‹ˆλ‹€.

μ–΄λ–»κ²Œ λ™μž‘ν•˜λ‚˜μš”?

 +-----------------+       +-------------+       +------------------+
 |   데이터 μ†ŒμŠ€     | pull  |  huh.json   | build |   Your App       |
 |                 |------>|  (JSON DSL) |------>|                  |
 |  기획자/PM 관리   |       |  νƒ€μž… μ•ˆμ „    |       |  μžλ™ μ—λŸ¬ UI λ Œλ” |
 +-----------------+       +-------------+       +------------------+

데이터 μ†ŒμŠ€: Google Sheets Β· Airtable Β· Notion Β· CSV Β· XLSX

데이터 μ†ŒμŠ€λŠ” μ΄λ ‡κ²Œ μƒκ²ΌμŠ΅λ‹ˆλ‹€:

trackId type message title action
ERR_NETWORK toast λ„€νŠΈμ›Œν¬ 연결이 λΆˆμ•ˆμ •ν•©λ‹ˆλ‹€.
ERR_AUTH modal {{userName}}λ‹˜μ˜ 인증이 λ§Œλ£Œλ˜μ—ˆμŠ΅λ‹ˆλ‹€. 인증 만료 둜그인 β†’ redirect:/login
ERR_NOT_FOUND page μš”μ²­ν•˜μ‹  νŽ˜μ΄μ§€κ°€ μ‘΄μž¬ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€. 404 λŒμ•„κ°€κΈ° β†’ back

μ½”λ“œλŠ” μ΄κ²ƒλ§Œ μž‘μ„±ν•˜λ©΄ λ©λ‹ˆλ‹€:

const { huh } = useHuh();

// trackId둜 직접 μ—λŸ¬ 트리거
huh('ERR_AUTH', { userName: '홍길동' });

// API μ—λŸ¬ μ½”λ“œλ₯Ό trackId둜 λ§€ν•‘ν•˜μ—¬ 트리거
huh(e.code); // 'API_500' β†’ errorMap β†’ 'ERR_SERVER'

λΉ λ₯Έ μ‹œμž‘

1. μ„€μΉ˜

# React
npm install @sanghyuk-2i/huh-core @sanghyuk-2i/huh-react

# Vue
npm install @sanghyuk-2i/huh-core @sanghyuk-2i/huh-vue

# Svelte
npm install @sanghyuk-2i/huh-core @sanghyuk-2i/huh-svelte

CDN (λ²ˆλ“€λŸ¬ 없이 μ‚¬μš©)

<script src="https://unpkg.com/@sanghyuk-2i/huh-core"></script>
<!-- window.HuhCore 둜 λͺ¨λ“  API μ‚¬μš© κ°€λŠ₯ -->

2. 데이터 μ†ŒμŠ€μ—μ„œ μ—λŸ¬ μ½˜ν…μΈ  κ°€μ Έμ˜€κΈ°

npx huh init          # .huh.config.ts 생성 (데이터 μ†ŒμŠ€ 선택)
npx huh pull          # 데이터 μ†ŒμŠ€ β†’ huh.json λ³€ν™˜

3. 앱에 Provider μ„€μ •

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

const renderers = {
  toast: ({ error, onDismiss }) => (
    <div className="toast" onClick={onDismiss}>
      {error.message}
    </div>
  ),
  modal: ({ error, onAction, onDismiss }) => (
    <div className="modal-overlay">
      <div className="modal">
        <h2>{error.title}</h2>
        <p>{error.message}</p>
        <button onClick={onAction}>{error.action?.label}</button>
        <button onClick={onDismiss}>λ‹«κΈ°</button>
      </div>
    </div>
  ),
  page: ({ error, onAction }) => (
    <div className="error-page">
      {error.image && <img src={error.image} />}
      <h1>{error.title}</h1>
      <p>{error.message}</p>
      <button onClick={onAction}>{error.action?.label}</button>
    </div>
  ),
};

function App() {
  return (
    <HuhProvider source={errorContent} renderers={renderers}>
      <MyPage />
    </HuhProvider>
  );
}

4. μ–΄λ””μ„œλ“  μ—λŸ¬ 처리

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

  const fetchData = async () => {
    try {
      await api.getData();
    } catch (e) {
      // trackId둜 직접 트리거
      huh('ERR_FETCH_FAILED', { userName: '홍길동' });

      // λ˜λŠ” API μ—λŸ¬ μ½”λ“œλ‘œ 트리거 (errorMap 경유)
      huh(e.code);
    }
  };

  return <button onClick={fetchData}>데이터 쑰회</button>;
}

μ£Όμš” κΈ°λŠ₯

ν…œν”Œλ¦Ώ λ³€μˆ˜

μ‹œνŠΈμ—μ„œ {{variable}} 문법을 μ‚¬μš©ν•˜μ„Έμš”. λŸ°νƒ€μž„μ— λ³€μˆ˜κ°€ μΉ˜ν™˜λ©λ‹ˆλ‹€:

μ‹œνŠΈ:   "{{userName}}λ‹˜, {{count}}건의 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€."
μ½”λ“œ:   huh('ERR_BATCH', { userName: '홍길동', count: '3' })
κ²°κ³Ό:   "ν™κΈΈλ™λ‹˜, 3건의 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€."

3κ°€μ§€ μ—λŸ¬ νƒ€μž…

νƒ€μž… μš©λ„ μ˜ˆμ‹œ
toast μ§§κ³  κ°„λ‹¨ν•œ μ•Œλ¦Ό λ„€νŠΈμ›Œν¬ 였λ₯˜, μ €μž₯ μ‹€νŒ¨
modal μ‚¬μš©μž 확인이 ν•„μš”ν•œ 경우 인증 만료, κΆŒν•œ λΆ€μ‘±
page 전체 ν™”λ©΄ μ—λŸ¬ μƒνƒœ 404, 점검 쀑, 치λͺ…적 였λ₯˜

μžλ™ μ•‘μ…˜ 처리

μ‹œνŠΈμ—μ„œ μ•‘μ…˜μ„ μ •μ˜ν•˜λ©΄, Huhκ°€ λ™μž‘μ„ μžλ™μœΌλ‘œ μ²˜λ¦¬ν•©λ‹ˆλ‹€:

μ•‘μ…˜ νƒ€μž… λ™μž‘
redirect μ§€μ •λœ URL둜 이동
retry μ—λŸ¬ μ΄ˆκΈ°ν™” + onRetry 콜백 μ‹€ν–‰
back history.back() 호좜
dismiss μ—λŸ¬ μ΄ˆκΈ°ν™”

λΉŒλ“œ νƒ€μž„ μœ νš¨μ„± 검증

npx huh validate

# βœ“ 12개 μ—λŸ¬ ν•­λͺ© λ‘œλ“œ
# ⚠ WARN_TOAST_TITLE: toast νƒ€μž…μ—λŠ” title이 λΆˆν•„μš”ν•©λ‹ˆλ‹€
# βœ— ERR_REDIRECT: redirect μ•‘μ…˜μ—λŠ” target URL이 ν•„μš”ν•©λ‹ˆλ‹€

CI/CD νŒŒμ΄ν”„λΌμΈμ— μ ν•©ν•©λ‹ˆλ‹€. μ½˜ν…μΈ  였λ₯˜λ₯Ό ν”„λ‘œλ•μ…˜μ— 배포되기 전에 μž‘μ•„λƒ…λ‹ˆλ‹€.

νŒ¨ν‚€μ§€

νŒ¨ν‚€μ§€ μ„€λͺ…
@sanghyuk-2i/huh-core μ˜μ‘΄μ„± 제둜. νƒ€μž…, νŒŒμ‹±, ν…œν”Œλ¦Ώ μ—”μ§„, μœ νš¨μ„± 검증. CDN 지원.
@sanghyuk-2i/huh-react React 바인딩. HuhProvider + useHuh ν›….
@sanghyuk-2i/huh-vue Vue 3 바인딩. HuhProvider + useHuh composable.
@sanghyuk-2i/huh-svelte Svelte 5 바인딩. HuhProvider + useHuh.
@sanghyuk-2i/huh-cli init / pull / validate λͺ…λ Ήμ–΄.

@sanghyuk-2i/huh-coreλŠ” μ˜μ‘΄μ„±μ΄ μ „ν˜€ μ—†μœΌλ©° λͺ¨λ“  JavaScript λŸ°νƒ€μž„μ—μ„œ λ™μž‘ν•©λ‹ˆλ‹€. vanilla JSμ—μ„œλ„ λ‹¨λ…μœΌλ‘œ μ‚¬μš©ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

μ™œ HuhμΈκ°€μš”?

κΈ°μ‘΄ 방식 (μ‚°μž¬) Huh λ„μž… ν›„
μ—λŸ¬ 문ꡬ μ»΄ν¬λ„ŒνŠΈμ— ν•˜λ“œμ½”λ”© μ™ΈλΆ€ 데이터 μ†ŒμŠ€μ—μ„œ 관리
문ꡬ μˆ˜μ • μ½”λ“œ λ³€κ²½ + 배포 ν•„μš” μ‹œνŠΈ μˆ˜μ • β†’ huh pull
μˆ˜μ • κ°€λŠ₯ 인원 개발자만 μ‹œνŠΈ μ ‘κ·Ό κΆŒν•œμ΄ μžˆλŠ” λˆ„κ΅¬λ‚˜
일관성 κ°œλ°œμžλ§ˆλ‹€ λ‹€λ₯Έ νŒ¨ν„΄ ν•˜λ‚˜μ˜ νŒ¨ν„΄, λͺ¨λ“  κ³³μ—μ„œ
νƒ€μž… μ•ˆμ „μ„± μ—†μŒ μ™„μ „ν•œ TypeScript 지원
μœ νš¨μ„± 검증 μ—†μŒ λΉŒλ“œ νƒ€μž„ + CI 검증

ν…œν”Œλ¦Ώ

각 데이터 μ†ŒμŠ€μ— λ§žλŠ” ν…œν”Œλ¦Ώμ„ 볡사/λ‹€μš΄λ‘œλ“œν•˜μ—¬ λ°”λ‘œ μ‹œμž‘ν•  수 μžˆμŠ΅λ‹ˆλ‹€:

데이터 μ†ŒμŠ€ ν…œν”Œλ¦Ώ
Google Sheets ν…œν”Œλ¦Ώ 볡사
Airtable ν…œν”Œλ¦Ώ 볡제
Notion ν…œν”Œλ¦Ώ 볡제
XLSX λ‹€μš΄λ‘œλ“œ
CSV ν•œκ΅­μ–΄ Β· English

λ¬Έμ„œ

CI/CD 연동

# .github/workflows/sync-errors.yml
- name: μ—λŸ¬ μ½˜ν…μΈ  동기화
  run: npx huh pull
  env:
    # μ‚¬μš©ν•˜λŠ” 데이터 μ†ŒμŠ€μ— λ§žλŠ” ν‚€λ₯Ό μ„€μ •ν•˜μ„Έμš”
    GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }} # Google Sheets
    # AIRTABLE_API_KEY: ${{ secrets.AIRTABLE_API_KEY }}   # Airtable
    # NOTION_API_KEY: ${{ secrets.NOTION_API_KEY }}       # Notion

- name: μœ νš¨μ„± 검증
  run: npx huh validate

- name: λ³€κ²½ 사항 컀밋
  run: |
    git add src/huh.json
    git commit -m "chore: sync error content" || true

κΈ°μ—¬ν•˜κΈ°

git clone https://github.com/your-org/huh.git
cd huh
pnpm install
pnpm build
pnpm test

이 λͺ¨λ…Έλ ˆν¬λŠ” Turborepo와 pnpm workspacesλ₯Ό μ‚¬μš©ν•©λ‹ˆλ‹€.

λΌμ΄μ„ μŠ€

MIT

Top categories

Loading Svelte Themes