Appearance
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
| 필드 | 타입 | 설명 |
|---|---|---|
providers | Constructor[] | @Injectable()로 표시된 서비스 클래스 |
controllers | Constructor[] | @Controller()로 표시된 핸들러 클래스 |
모듈 구성 패턴
기능 단위로 모듈을 분리하고, createApp의 modules 배열에 나열한다:
ts
createApp({
modules: [
FileModule,
SettingsModule,
ThemeModule,
],
// ...
});각 모듈의 provider는 등록 순서대로 resolve된다. 모듈 간 의존성이 있다면 의존되는 모듈을 먼저 나열한다.
ts
// SettingsModule의 SettingsService가 FileModule의 FileService를 inject하는 경우
modules: [
FileModule, // 먼저 등록
SettingsModule, // FileService를 inject 가능
]다음 단계
- Dependency Injection — Container와 inject()의 동작 원리
- Guard & ExceptionHandler — 핸들러 파이프라인