Code.
sspzoa.io 코드 컨벤션. UI 카피는 한국어(lang="ko"), 식별자·주석·커밋은 영어. 게이트는 bun run lint 하나. 비주얼 토큰은 Design System.
Principles
에이전트·기여자가 같은 결정을 반복하지 않도록 고정한 규칙.
- Server by default — 인터랙션이 필요할 때만
"use client". - Named exports — Next.js 엔트리(page/layout/error/metadata)만 default export.
- No hand-rolled memo — React Compiler 사용.
useMemo/useCallback/React.memo금지에 가깝게. - Tokens only — 임의 px·hex 금지. 스케일은 /design-system.
- Bun only — npm / yarn / pnpm 쓰지 않음. bun.lock 커밋.
- No drive-by docs — 요청 없이 README·주석 추가하지 않음. 주석도 기본적으로 넣지 않음.
Stack
고정 선택. 대체 도구를 새로 들이지 않음.
FrameworkNext.js 16 App RouterUIReact 19 + React CompilerLanguageTypeScript 5 strict · @/* → ./src/*StylingTailwind CSS v4 · token-onlyLint / formatBiome 2.2 (no ESLint, no Prettier)ValidationZod 4Iconslucide-reactCMSNotion API 2025-09-03Package managerBun · Node ≥ 20Analytics@vercel/analytics · speed-insights
Commands
변경 완료 전 항상 lint. 프로젝트의 유일한 로컬 게이트.
bun install
bun dev
bun run build
bun start
bun run lint # biome check
bun run lint:fix # biome check --write
bun run format # biome format --writeStructure
features = 페이지 전용. shared = 재사용. app = 라우트 엔트리.
src/
app/ # routes, layout, metadata
features/ # page-specific UI + sections
sections/ # async Server Component sections
shared/
ui/ # presentational primitives
lib/ # env, notion, portfolio-data, errors
utils/ # formatDate, formatPeriod
markdown/ # server-safe markdown-lite
schemas.ts # Zod + z.infer types
types.ts # re-exports- features/sections — 섹션마다 데이터 fetch + 에러 바운더리 소유.
- shared/ui — 도메인 몰라도 쓸 수 있는 프레젠테이션 프리미티브.
- shared/lib — env, Notion 클라이언트, portfolio-data, errors.
TypeScript
strict. 스키마가 타입의 정본(source of truth).
stricttruepaths@/* → "./src/*"Entity typesz.infer from shared/schemas.tsRe-exportsshared/types.ts
Imports
- Path alias — 상대 경로 대신
@/…. - type imports — 타입만 쓸 때
import type. - Organize imports — Biome assist가 정리. 수동 정렬 금지.
React
RSC 기본. 클라이언트는 스크롤 관찰·토글 등 상태/브라우저 API가 필요할 때만.
Server vs client (today)
Serversections/*, Description, pages, layoutClientnav, side-project-toggle, collapsible
Section error pattern
export async function XSection({ index, id }: SectionComponentProps) {
try {
const items = await fetchX();
if (items.length === 0) return null;
return (
<Section id={id} title="X" index={index} count={items.length}>
…
</Section>
);
} catch (error) {
console.error("[XSection]", error);
return (
<Section id={id} title="X" index={index}>
<p className="text-content-standard-secondary text-label">
{getErrorMessage(error)}
</p>
</Section>
);
}
}getErrorMessage는 @/shared/lib/errors에서 import. 로컬 재정의 금지. empty → null.
Images
- next/image — 항상
width/height/sizes,draggable={false}. 장식 이미지는alt="". - Remote hosts — 새 호스트는
next.config.ts의remotePatterns와 CSPimg-src둘 다.
Format
Biome 단일 툴체인. CSS는 Biome 제외 — globals.css는 수동 포맷.
indent2 spaceslineWidth120quotesdouble (JSX too)semicolonsalwaystrailingCommasall (JSON: none)bracketSameLinetrue (JSX > on last prop line)arrowParenthesesalwayslineEndinglfuseSortedClasseswarn · lint:fix sortsuseArrowFunctionerrornoUnusedImportserror
<Button
text="Save"
onClick={handleSave}
/>Naming
영어 식별자. 파일은 kebab-case.
Fileskebab-case.tsx · aboutme.tsx, project-card.tsxComponentsPascalCase named export · export function ChipHooksuseActiveSection (colocate in feature file OK)FetchersfetchProjects, getPortfolioDataErrorsDataFetchError, DataValidationError, NotionApiErrorEnv varsNOTION_TOKEN, *_DATA_SOURCE_ID (UUID)
Styling
디자인 토큰만. Layer 1 --solid-* 는 globals.css 전용.
- Semantic classes —
text-content-standard-primary,p-spacing-500,rounded-radius-md,duration-fast. - No theme toggle —
prefers-color-schemeonly. Providers는 pass-through. - Motion — 상태 변화(hover/focus/active)만. 스크롤 리빌 장식 금지. reduced-motion 존중.
- tracking-wider / widest — Tailwind 빌트인 사용 허용 (토큰 위반 아님).
- Focus ring —
focus-visible:ring-2 focus-visible:ring-core-accent/50 focus-visible:ring-offset-2 focus-visible:ring-offset-background-standard-primary
전체 스케일·컴포넌트 쇼케이스 → /design-system
Data
Notion이 소스 오브 트루스. 요청마다 live fetch — 캐시 없음(force-dynamic + no-store).
.env.local
→ env.ts (Zod at import)
→ portfolio-data.ts → notion.ts
→ schema.parse()
→ Server Component section- aboutMe required — getPortfolioData에서 aboutMe 실패는 전파. 나머지 섹션은 []로 degrade.
- Error kinds — DataFetchError (4xx → config), DataValidationError (Zod).
- Dates — ISO →
YYYY.MMvia formatDate / formatPeriod. - No caching by design — 신선도 트레이드오프 없이 캐시 추가하지 말 것.
A11y & security
새 마크업은 접근 가능해야 함. CSP·보안 헤더는 next.config.ts.
- Skip link — layout의
#main-content유지. - aria-current / aria-expanded — 네비·Collapsible 패턴 유지.
- dangerouslySetInnerHTML — JSON-LD Person 스키마 한 곳만. 새로 도입 금지.
- Secrets — 로그·커밋에 토큰 금지. env는 Zod로 import-time 검증.
- CSP — 외부 script/style/font/img/connect 추가 시 해당 *-src 갱신.