Skip to content

Module System

모듈 시스템은 4개의 데코레이터로 구성된다. TC39 표준 데코레이터를 사용하므로 experimentalDecorators 설정이 필요 없다.

@Injectable()

DI 컨테이너에 등록 가능한 서비스를 표시한다.

ts
import { Injectable } from '@repo/electron-ipc';

@Injectable()
class UserService {
  findById(id: string) {
    // ...
  }
}

@Injectable()이 없는 클래스를 provider로 등록하면 무시된다.

@Controller(options?)

IPC 핸들러를 포함하는 클래스를 표시한다. @Handle@Forward로 등록된 메서드 메타데이터를 수집한다.

ts
import { Controller } from '@repo/electron-ipc';

@Controller()
class UserController {
  // @Handle, @Forward 메서드들이 여기에 위치
}

api 옵션을 지정하면 codegen이 해당 이름으로 preload API를 생성한다:

ts
@Controller({ api: 'userAPI' })
class UserController {
  // ...
}

내부 동작

@Handle@Forward 데코레이터는 메서드 메타데이터를 임시 버퍼에 저장한다. @Controller 데코레이터가 클래스에 적용될 때 버퍼를 드레인하여 클래스에 바인딩한다. 이 순서는 TC39 데코레이터 실행 규칙(메서드 → 클래스)에 의해 보장된다.

@Handle(channel)

메서드를 IPC 채널에 바인딩한다. @Controller() 클래스 내에서만 사용한다.

ts
import { Controller, Handle, inject } from '@repo/electron-ipc';

@Controller()
class FileController {
  private readonly fileService = inject(FileService);

  @Handle('file:read')
  async readFile(path: string): Promise<string> {
    return this.fileService.read(path);
  }

  @Handle('file:write')
  async writeFile(path: string, content: string): Promise<void> {
    await this.fileService.write(path, content);
  }
}

핸들러 메서드의 인자는 렌더러에서 ipcRenderer.invoke(channel, ...args)로 전달한 값이 그대로 들어온다.

@Forward(channel)

Service의 EventSource를 IPC 채널로 전달한다. 메서드는 EventSource를 반환해야 한다. 프레임워크가 onWindowAttach 시점에 구독하여 webContents.send()로 렌더러에 push한다. 윈도우 closed 시 구독이 자동 해제된다.

ts
import { Controller, Forward, Handle, inject } from '@repo/electron-ipc';
import { EventSource } from '@repo/electron-ipc';

@Controller({ api: 'themeAPI' })
class ThemeController {
  private readonly theme = inject(ThemeService);

  @Handle('theme:get')
  getTheme() {
    return this.theme.getThemeInfo();
  }

  @Forward('theme:changed')
  onThemeChanged() {
    return this.theme.themeChanged; // EventSource<ThemeInfo>
  }
}

Service 측에서는 EventSource를 필드로 선언하고 emit()으로 이벤트를 발행한다:

ts
import { Injectable } from '@repo/electron-ipc';
import { EventSource } from '@repo/electron-ipc';

@Injectable()
class ThemeService {
  readonly themeChanged = new EventSource<ThemeInfo>();

  setTheme(theme: Theme) {
    // ... 테마 적용 로직
    this.themeChanged.emit(newThemeInfo);
  }
}

이 패턴은 Service에서 OnWindowAttach 구현 + window 필드 + webContents.send() 직접 호출을 제거한다.

@Module(metadata)

provider와 controller를 하나의 모듈 단위로 그룹화한다.

ts
import { Module } from '@repo/electron-ipc';

@Module({
  providers: [FileService, LogService],
  controllers: [FileController],
})
class FileModule {}

ModuleMetadata

필드타입설명
providersConstructor[]@Injectable()로 표시된 서비스 클래스
controllersConstructor[]@Controller()로 표시된 핸들러 클래스

모듈 구성 패턴

기능 단위로 모듈을 분리하고, createAppmodules 배열에 나열한다:

ts
createApp({
  modules: [
    FileModule,
    SettingsModule,
    ThemeModule,
  ],
  // ...
});

각 모듈의 provider는 등록 순서대로 resolve된다. 모듈 간 의존성이 있다면 의존되는 모듈을 먼저 나열한다.

ts
// SettingsModule의 SettingsService가 FileModule의 FileService를 inject하는 경우
modules: [
  FileModule,      // 먼저 등록
  SettingsModule,  // FileService를 inject 가능
]

다음 단계