본문으로 건너뛰기

새 기능 추가하기

이 가이드는 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

  1. routers/index.tsregisterAllRouters() 안에서 Service와 Router 인스턴스 생성
  2. router.register() 호출
  3. preload/index.tsapi 객체에 메서드 노출

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줄
커스텀 Hook300줄400줄600줄
유틸리티 함수150줄200줄300줄
타입 정의100줄150줄200줄
Service300줄400줄600줄

경고 기준선을 초과한 파일은 현재 작업 내에서 분리해야 하며, 나중으로 미룰 수 없습니다.

TypeScript

npm run typecheck

5단계: 테스트

테스트 명령

명령설명
npm run testVitest 전체 테스트 실행
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-index Skill 업데이트됨
  • elfi-kb 지식 베이스 업데이트됨 (사용자에게 노출되는 기능 추가 시)
  • PR 설명에 변경 사항이 명확히 기술됨