μλ¬ λ©μμ§λ μ½λκ° μλλΌ μ€νλ λμνΈμμ κ΄λ¦¬νμΈμ.
λΉκ°λ°μλ Google Sheets, Airtable, Notion λ±μμ μλ¬ μ½ν μΈ λ₯Ό κ΄λ¦¬νκ³ , κ°λ°μλ νμ μμ ν μλ¬ UIλ₯Ό μλμΌλ‘ λ λλ§ν©λλ€.
λͺ¨λ νλ‘ νΈμλ νμ κ²°κ΅ μλ¬ λ©μμ§κ° μ½λλ² μ΄μ€ μ¬κΈ°μ κΈ°μ ν©μ΄μ§λ λ¬Έμ λ₯Ό κ²ͺμ΅λλ€. κΈ°νμκ° λ¬Έκ΅¬ λ³κ²½μ μμ²νλ©΄ ν°μΌμ λ§λ€κ³ , κ°λ°μκ° μ½λλ₯Ό μμ νκ³ , λ°°ν¬κΉμ§ κΈ°λ€λ €μΌ ν©λλ€. μλ¬ μ²λ¦¬ λ‘μ§μ μ»΄ν¬λνΈλ§λ€ μ κ°κ°μ λλ€. μ§μ€μ μμ²(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'
# 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
<script src="https://unpkg.com/@sanghyuk-2i/huh-core"></script>
<!-- window.HuhCore λ‘ λͺ¨λ API μ¬μ© κ°λ₯ -->
npx huh init # .huh.config.ts μμ± (λ°μ΄ν° μμ€ μ ν)
npx huh pull # λ°μ΄ν° μμ€ β huh.json λ³ν
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>
);
}
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건μ μ€λ₯κ° λ°μνμ΅λλ€."
| νμ | μ©λ | μμ |
|---|---|---|
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 pull |
| μμ κ°λ₯ μΈμ | κ°λ°μλ§ | μνΈ μ κ·Ό κΆνμ΄ μλ λꡬλ |
| μΌκ΄μ± | κ°λ°μλ§λ€ λ€λ₯Έ ν¨ν΄ | νλμ ν¨ν΄, λͺ¨λ κ³³μμ |
| νμ μμ μ± | μμ | μμ ν TypeScript μ§μ |
| μ ν¨μ± κ²μ¦ | μμ | λΉλ νμ + CI κ²μ¦ |
κ° λ°μ΄ν° μμ€μ λ§λ ν νλ¦Ώμ 볡μ¬/λ€μ΄λ‘λνμ¬ λ°λ‘ μμν μ μμ΅λλ€:
| λ°μ΄ν° μμ€ | ν νλ¦Ώ |
|---|---|
| Google Sheets | ν νλ¦Ώ λ³΅μ¬ |
| Airtable | ν νλ¦Ώ 볡μ |
| Notion | ν νλ¦Ώ 볡μ |
| XLSX | λ€μ΄λ‘λ |
| CSV | νκ΅μ΄ Β· English |
parseSheetData, resolveError, validateConfigHuhProvider, useHuh, λ λλ¬ νμ
# .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λ₯Ό μ¬μ©ν©λλ€.