Skip to content

Controllers

Controller는 IPC 요청을 처리하는 핸들러 클래스다. @Controller(contract)로 도메인 contract와 연결하면 메서드 이름이 contract 키와 1:1로 매칭되어 자동으로 IPC 채널에 바인딩된다. 비즈니스 로직은 provider에 위임한다.

@Controller(contract)

@Controller()에 contract를 전달하면 두 가지가 동시에 일어난다:

  1. 클래스 형태 자동 강제 — TS가 ControllerOf<C> 형태로 클래스 시그니처를 검사한다. 메서드 누락이나 시그니처 mismatch는 IDE에서 빨간 줄로 잡힌다.
  2. 자동 채널 바인딩 — 부트스트랩 시점에 contract.handlescontract.forwards를 순회하며 동일한 이름의 메서드를 IPC 채널에 바인딩한다.
ts
import { Controller, inject } from '@repo/electron-ipc';
import { userContract } from '@shared/user/contract';
import { UserService } from './service';

@Controller(userContract)
class UserController {
  private readonly users = inject(UserService);

  // contract.handles.listUsers 키 → 'user:list' 채널 자동 바인딩
  listUsers() {
    return this.users.list();
  }

  // contract.handles.getUser 키 → 'user:get' 채널
  getUser(id: string) {
    return this.users.get(id);
  }
}

contract 정의에서 채널 문자열을 한 번만 적으면 끝이다. controller는 메서드 이름만으로 contract와 묶인다.

Contract = 추상 인터페이스

@Controller(contract)는 contract를 추상 인터페이스처럼 다룬다. contract에 정의된 모든 handle/forward 키에 해당하는 메서드를 controller가 구현해야 하고, 시그니처도 일치해야 한다. implements를 따로 적을 필요가 없다 — 데코레이터가 자동으로 강제한다.

시그니처 검증

contract와 어긋난 controller는 컴파일 단계에서 막힌다.

ts
const userContract = contract({
  handles: {
    getUser: handle<[string], User>('user:get'),
  },
});

@Controller(userContract)
class UserController {
  getUser(id: number) { ... }   // ❌ TS 에러: id가 number — string이 와야 함
  // getUser 누락 시: Property 'getUser' is missing
}

에러는 @Controller(...) 라인에 표시되며, 어느 메서드의 어느 부분이 어긋났는지 짚어준다.

forward 메서드

contract의 forwards에 정의된 키는 controller에서 EventSource<T>를 반환하는 메서드로 구현한다. 프레임워크가 onWindowAttach 시점에 구독하여 webContents.send()로 렌더러에 push한다.

ts
const appContract = contract({
  handles: {},
  forwards: {
    onThemeChanged: forward<ResolvedTheme>('theme:changed'),
  },
});

@Controller(appContract)
class AppController {
  private readonly theme = inject(ThemeService);

  onThemeChanged() {
    return this.theme.themeChanged; // EventSource<ResolvedTheme>
  }
}

자세한 EventSource 패턴은 EventSource 참고.

메서드별 Pipe — @UsePipes

특정 핸들러에만 추가 Pipe를 적용하려면 @UsePipes를 메서드 데코레이터로 단다. 글로벌 Pipe 이후에 실행된다.

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

@Controller(terminalContract)
class TerminalController {
  private readonly service = inject(TerminalService);

  @UsePipes(new CwdPipe())
  spawn(params: TerminalSpawnParams) {
    return this.service.spawn(params);
  }
}

다음 단계