새 기능 추가하기
이 가이드는 Elftia에서 완전한 기능을 구현하는 엔드-투-엔드 프로세스를 설명합니다. IPC 라우트 추가하기를 기반으로, 계획, 테스트, 문서 업데이트 등 전체 생명주기를 다룹니다.
개요: 기능 개발 흐름
계획 → 백엔드 구현 → 프론트엔드 구현 → 품질 검사 → 테스트 → 문서 업데이트
flowchart LR
A[계획] --> B[백엔드]
B --> C[프론트엔드]
C --> D[품질 검사]
D --> E[테스트]
E --> F[문서 업데이트]
F --> G[PR 제출]
1단계: 계획
코드 작성 전에 다음 질문들을 명확히 답해 두세요.
요구 사항 분석 체크리스트
| 질문 | 목적 |
|---|---|
| 어떤 공유 타입이 필요한가? | @shared/contracts/ 하위의 타입 파일 결정 |
| 데이터베이스 테이블 작업이 포함되는가? | 새 마이그레이션이 필요한지 여부 결정 |
| 어떤 Service 로직이 필요한가? | 백엔드 모듈 결정 |
| 어떤 IPC 채널이 필요한가? | Router와 Preload API 결정 |
| 어떤 UI 컴포넌트가 필요한가? | 프론트엔드 파일 구조 결정 |
| 새 라우팅 페이지를 추가하는가? | 지연 로딩 및 라우트 등록 필요 여부 결정 |
| 사용자에게 노출되는 기능인가? | 지식 베이스 문서 업데이트 필요 여부 결정 |
파일 계획 템플릿
"노트" 기능을 예시로:
packages/
├── desktop/app/
│ ├── shared/contracts/
│ │ └── note-types.ts # 공유 타입
│ └── main/
│ ├── services/content/workspace/notes/
│ │ ├── NoteFileService.ts # 파일 CRUD Service
│ │ └── NoteWatcher.ts # 파일 감시 Service
│ └── services/routers/
│ └── NoteRouter.ts # IPC Router
│
├── renderer/src/
│ ├── features/notes/
│ │ ├── hooks/
│ │ │ └── useNotes.ts # 데이터 페칭 Hook
│ │ └── components/
│ │ ├── NotesList.tsx # 목록 컴포넌트
│ │ ├── NoteEditor.tsx # 에디터 컴포넌트
│ │ └── index.ts # Barrel export
│ └── locales/
│ ├── en/notes.json
│ ├── zh/notes.json
│ └── ja/notes.json
2단계: 백엔드 구현
다음 순서로 백엔드 코드를 작성합니다.
2.1 공유 타입
{/* packages/desktop/app/shared/contracts/note-types.ts */}
export interface Note {
id: string;
title: string;
content: string;
tags: string[];
createdAt: string;
updatedAt: string;
}
export interface CreateNoteInput {
title: string;
content?: string;
tags?: string[];
}
2.2 Service
Service는 모든 비즈니스 로직을 캡슐화합니다. 단일 책임 원칙을 따르세요 — 파일 읽기/쓰기와 파일 감시는 별도의 Service로 분리합니다.
services/content/workspace/notes/
├── NoteFileService.ts # 파일 CRUD 작업
└── NoteWatcher.ts # 파일 시스템 변경 감시
2.3 Router
secureHandle 패턴으로 IPC 핸들러를 등록합니다. 모든 파라미터는 Zod 검증을 통과해야 합니다.
자세한 단계는 IPC 라우트 추가하기를 참고하세요.
2.4 등록 + Preload
routers/index.ts의registerAllRouters()안에서 Service와 Router 인스턴스 생성router.register()호출preload/index.ts의api객체에 메서드 노출
3단계: 프론트엔드 구현
3.1 Hook 만들기
데이터 페칭 및 상태 관리 로직을 커스텀 Hook으로 캡슐화합니다.
{/* packages/renderer/src/features/notes/hooks/useNotes.ts */}
export function useNotes() {
const [notes, setNotes] = useState<Note[]>([]);
const [loading, setLoading] = useState(true);
// 데이터 페칭, CRUD 작업...
return { notes, loading, create, update, remove };
}
3.2 컴포넌트 만들기
컴포넌트는 UI 렌더링만 담당하며, 비즈니스 로직은 Hook에 위임합니다.
components/notes/
├── index.ts # Barrel export
├── NotesList.tsx # 목록 뷰 (400줄 미만)
├── NoteEditor.tsx # 에디터 (400줄 미만)
└── NoteCard.tsx # 단일 노트 카드
컴포넌트 기준:
- 하드코딩 색상 대신 시맨틱 토큰(
bg-surface-1,text-foreground) 사용 - 네이티브 HTML 컨트롤 대신 프로젝트 UI 컴포넌트(
Button,Input,Select) 사용 - 문구 국제화에
useTranslation()사용
3.3 라우트 등록 (새 페이지인 경우)
기능에 독립 페이지가 필요한 경우:
{/* packages/renderer/src/app/lazy/pages.tsx */}
export const LazyNotesPage = lazy(() => import('../../features/notes/NotesPage'));
export const SuspenseNotesPage = withSuspense(LazyNotesPage, PageSkeleton);
{/* packages/renderer/src/app/App.tsx — 기존 import 경로는 lazy/index.ts barrel re-export를 통해 유효 */}
import { SuspenseNotesPage as NotesPage } from './lazy';
<Route path="/notes" element={<NotesPage />} />
새 워크스페이스 타입을 추가하는 경우(새 페이지가 아닌 경우)에는 다른 경로를 따릅니다.
features/chat/components/content/workspaces/registry.ts를 수정하고,definitions/<Name>Workspace.tsx를 추가한 뒤,app/lazy/workspaces.tsx에 해당 지연 import를 추가하세요.packages/renderer/CLAUDE.md의 "새 워크스페이스 타입 추가하기" 섹션을 참고하세요.
3.4 i18n
세 가지 언어의 번역 파일을 만들고 i18n/index.ts에 네임스페이스를 등록합니다.
locales/
├── en/notes.json
├── zh/notes.json
└── ja/notes.json
4단계: 품질 검사
ESLint
# 수정한 파일 검사
npx eslint packages/renderer/src/features/notes/components/ packages/renderer/src/features/notes/hooks/
# import 순서 등 자동 수정
npx eslint packages/renderer/src/features/notes/components/ --fix
파일 크기 제한
| 파일 유형 | 권장 | 경고 기준선 | 하드 제한 |
|---|---|---|---|
| React 컴포넌트 | 400줄 | 600줄 | 800줄 |
| 커스텀 Hook | 300줄 | 400줄 | 600줄 |
| 유틸리티 함수 | 150줄 | 200줄 | 300줄 |
| 타입 정의 | 100줄 | 150줄 | 200줄 |
| Service | 300줄 | 400줄 | 600줄 |
경고 기준선을 초과한 파일은 현재 작업 내에서 분리해야 하며, 나중으로 미룰 수 없습니다.
TypeScript
npm run typecheck
5단계: 테스트
테스트 명령
| 명령 | 설명 |
|---|---|
npm run test | Vitest 전체 테스트 실행 |
npm run test -- --filter notes | 키워드로 테스트 필터링 |
수동 테스트 체크리스트
- 기능의 정상 경로(happy path)가 올바르게 동작함
- 오류 상황에서 올바른 메시지가 표시됨 (네트워크 오류, 데이터베이스 오류 등)
- 다크 모드와 라이트 모드에서 UI가 올바르게 표시됨
- 배경화면 투명도 활성화 시 UI가 올바르게 표시됨
- 창을 최소 너비로 줄였을 때 레이아웃이 깨지지 않음
6단계: 문서 업데이트
필수 문서 업데이트 항목
| 조건 | 업데이트할 문서 |
|---|---|
| Service / 컴포넌트 / Hook / Context 추가 | .claude/skills/architecture-index/SKILL.md |
| 사용자에게 노출되는 페이지 기능 추가 | docs/elfi-kb/ 지식 베이스 문서 |
| 페이지 라우트 추가 | docs/elfi-kb/INDEX.md 라우트 맵 |
| IPC 인터페이스 변경 | 이 문서 사이트의 IPC 채널 목록 |
일반 패턴 참고
패턴 1: CRUD Service
엔티티 관리 기능(노트, 즐겨찾기, 템플릿 등)에 적합합니다.
공유 타입 → Service(CRUD) → Router(Zod 검증) → Preload → Hook(useState+CRUD) → 목록 + 편집 컴포넌트
패턴 2: 스트리밍 이벤트
실시간 푸시가 필요한 기능(채팅, 생성 작업 등)에 적합합니다.
백엔드 Service가 이벤트 발행 → 메인 프로세스 webContents.send(channel, data) → Preload onXxx 리스너 → Hook 구독/구독 해제
{/* Preload 측 — 이벤트 구독 패턴 */}
onEvent: (cb: (data: any) => void) => {
const channel = 'myFeature:event';
const handler = (_event: IpcRendererEvent, data: any) => cb(data);
ipcRenderer.on(channel, handler);
return () => ipcRenderer.removeListener(channel, handler);
},
{/* Hook 측 — 구독 관리 */}
useEffect(() => {
const unsubscribe = window.api.myFeature.onEvent((data) => {
setMessages((prev) => [...prev, data]);
});
return unsubscribe;
}, []);
패턴 3: 파일 작업
사용자 파일을 읽거나 써야 하는 기능(프로젝트 관리, 내보내기 등)에 적합합니다.
프론트엔드가 파일 경로/설정 전달 → 백엔드 Service가 파일 시스템 작업 수행 → 결과 URL 또는 내용 반환
핵심 원칙: 모든 파일 시스템 작업은 백엔드에서 완료합니다. 프론트엔드는 경로와 파라미터만 전달하며, 파일을 직접 조작하지 않습니다.
전체 체크리스트
백엔드
- 공유 타입이
@shared/contracts/에 정의됨 - Service가 모든 비즈니스 로직을 캡슐화함
- Router가
secureHandle+ Zod 검증을 사용함 - Router가
registerAllRouters()에 등록됨 - Preload API 메서드 시그니처가 올바름
-
DesktopApi타입 인터페이스가 업데이트됨
프론트엔드
- 커스텀 Hook이 데이터 페칭 로직을 캡슐화함
- 컴포넌트가 시맨틱 토큰과 프로젝트 UI 컴포넌트를 사용함
- 새 페이지에
React.lazy지연 로딩이 적용됨 - 세 가지 언어의 i18n 파일이 생성됨
품질
-
npm run lint오류 없이 통과 -
npm run typecheck통과 - 파일 크기가 제한 내에 있음
- 다크/라이트 모드 테스트 통과
- 배경화면 투명도 모드 테스트 통과
문서
-
architecture-indexSkill 업데이트됨 -
elfi-kb지식 베이스 업데이트됨 (사용자에게 노출되는 기능 추가 시) - PR 설명에 변경 사항이 명확히 기술됨