"규모가 커지면 main.js 하나에 모든 코드를 넣을 수 없다" — 계층을 분리하고 의존성을 관리해야 유지보수가 가능합니다.


계층 분리 아키텍처

PLAINTEXT
src/
├── main/
│   ├── index.ts                # 진입점 (앱 초기화)
│   ├── modules/                # 기능별 모듈
│   │   ├── window/
│   │   │   ├── WindowManager.ts
│   │   │   └── WindowFactory.ts
│   │   ├── file/
│   │   │   ├── FileService.ts
│   │   │   └── FileHandlers.ts  # IPC 핸들러
│   │   └── update/
│   │       └── UpdateService.ts
│   ├── services/               # 인프라 서비스
│   │   ├── Database.ts
│   │   ├── Logger.ts
│   │   └── ConfigStore.ts
│   └── ipc/                    # IPC 라우터
│       └── registerHandlers.ts
├── preload/
│   └── index.ts
└── renderer/
    └── src/

서비스 레이어 패턴

TYPESCRIPT
// main/services/Database.ts
interface DatabasePort {
  query<T>(sql: string, params?: unknown[]): T[];
  run(sql: string, params?: unknown[]): { changes: number };
}

class SQLiteDatabase implements DatabasePort {
  private db: BetterSqlite3.Database;

  constructor(dbPath: string) {
    this.db = new Database(dbPath);
    this.db.pragma('journal_mode = WAL');
  }

  query<T>(sql: string, params: unknown[] = []): T[] {
    return this.db.prepare(sql).all(...params) as T[];
  }

  run(sql: string, params: unknown[] = []) {
    return this.db.prepare(sql).run(...params);
  }
}
TYPESCRIPT
// main/modules/file/FileService.ts
class FileService {
  constructor(
    private db: DatabasePort,
    private logger: Logger,
  ) {}

  async getRecentFiles(limit = 10) {
    this.logger.info('최근 파일 조회');
    return this.db.query(
      'SELECT * FROM recent_files ORDER BY opened_at DESC LIMIT ?',
      [limit]
    );
  }

  async openFile(filePath: string) {
    const content = await fs.promises.readFile(filePath, 'utf-8');
    this.db.run(
      'INSERT OR REPLACE INTO recent_files (path, opened_at) VALUES (?, ?)',
      [filePath, new Date().toISOString()]
    );
    this.logger.info(`파일 열기: ${filePath}`);
    return content;
  }
}

IPC 핸들러 분리

TYPESCRIPT
// main/modules/file/FileHandlers.ts
export function registerFileHandlers(fileService: FileService) {
  ipcMain.handle('file:open', async (_event, filePath: string) => {
    return fileService.openFile(filePath);
  });

  ipcMain.handle('file:recent', async () => {
    return fileService.getRecentFiles();
  });
}
TYPESCRIPT
// main/ipc/registerHandlers.ts
export function registerAllHandlers(container: Container) {
  registerFileHandlers(container.get('fileService'));
  registerNoteHandlers(container.get('noteService'));
  registerSettingsHandlers(container.get('configStore'));
}

간단한 DI 컨테이너

TYPESCRIPT
// main/container.ts
class Container {
  private instances = new Map<string, unknown>();
  private factories = new Map<string, () => unknown>();

  register(name: string, factory: () => unknown) {
    this.factories.set(name, factory);
  }

  get<T>(name: string): T {
    if (!this.instances.has(name)) {
      const factory = this.factories.get(name);
      if (!factory) throw new Error(`서비스를 찾을 수 없음: ${name}`);
      this.instances.set(name, factory());
    }
    return this.instances.get(name) as T;
  }
}

// 초기화
const container = new Container();

container.register('logger', () => new Logger());
container.register('database', () => new SQLiteDatabase(dbPath));
container.register('configStore', () => new ConfigStore());
container.register('fileService', () =>
  new FileService(container.get('database'), container.get('logger'))
);
container.register('windowManager', () =>
  new WindowManager(container.get('configStore'))
);

앱 초기화

TYPESCRIPT
// main/index.ts
import { app } from 'electron';
import { Container } from './container';
import { registerAllHandlers } from './ipc/registerHandlers';

async function bootstrap() {
  const container = createContainer();

  // IPC 핸들러 등록
  registerAllHandlers(container);

  // 윈도우 생성
  const windowManager = container.get<WindowManager>('windowManager');
  windowManager.createMainWindow();
}

app.whenReady().then(bootstrap);

면접 포인트 정리

  • 메인 프로세스 코드를 서비스/핸들러/모듈로 계층 분리
  • IPC 핸들러는 비즈니스 로직에서 분리하여 테스트 용이하게
  • DI 컨테이너로 서비스 간 의존성을 관리
  • 포트/어댑터 패턴으로 데이터베이스 등 인프라를 교체 가능하게

아키텍처 패턴을 다뤘으면, 다음은 Webview vs BrowserView를 비교해봅시다.

댓글 로딩 중...