Angular `runInInjectionContext` 마이그레이션 가이드

오준석·2025년 8월 13일
0

코딩삽질방지

목록 보기
60/62

Angular v16부터 도입된 runInInjectionContext는 의존성 주입(DI) 컨텍스트가 없는 곳에서 DI 컨텍스트가 필요한 함수를 실행할 수 있게 해주는 강력한 API입니다. 이 가이드에서는 runInInjectionContext가 필요한 이유를 알아보고, 기존 코드를 새로운 방식으로 마이그레이션하는 방법을 안내합니다.

runInInjectionContext가 필요한가요?

Angular의 의존성 주입 시스템은 기본적으로 클래스의 constructor 내부와 같이 명확한 "주입 컨텍스트" 내에서 동기적으로 작동합니다. 하지만 다음과 같은 상황에서는 주입 컨텍스트 외부에서 서비스를 주입해야 할 필요가 있습니다.

  • RxJS 파이프라인 내의 커스텀 오퍼레이터
  • setTimeout, setInterval과 같은 비동기 콜백 함수
  • 동적으로 생성되는 컴포넌트의 콜백 함수
  • 함수형 라우터 가드/리졸버

과거에는 Injector.runInContext()를 사용하여 이를 해결했지만, 코드가 다소 장황하고 직관적이지 않았습니다. runInInjectionContext는 이 과정을 훨씬 더 간결하고 명확하게 만들어줍니다.

기존 방식: Injector.runInContext()

runInInjectionContext가 도입되기 전에는 Injector를 직접 주입받아 runInContext() 메서드를 사용했습니다.

// logger.service.ts
@Injectable({ providedIn: 'root' })
export class LoggerService {
  log(message: string) {
    console.log(`[LOG]: ${message}`);
  }
}

// old-way.component.ts
@Component({
  selector: 'app-old-way',
  template: '...',
})
export class OldWayComponent {
  private injector = inject(Injector);

  logAfterDelay(message: string) {
    setTimeout(() => {
      // setTimeout 콜백은 주입 컨텍스트가 아닙니다.
      this.injector.runInContext(() => {
        const logger = inject(LoggerService);
        logger.log(message);
      });
    }, 1000);
  }
}

이 방식은 잘 작동하지만, runInContext 콜백을 위한 추가적인 클로저가 필요하여 코드가 깊어지고 가독성이 떨어질 수 있습니다.

새로운 방식: runInInjectionContext

runInInjectionContextInjector 인스턴스의 메서드로 제공되어 훨씬 더 자연스럽게 코드를 작성할 수 있습니다.

위 예제를 runInInjectionContext로 마이그레이션해 보겠습니다.

// new-way.component.ts
@Component({
  selector: 'app-new-way',
  template: '...',
})
export class NewWayComponent {
  private injector = inject(Injector);

  logAfterDelay(message: string) {
    setTimeout(() => {
      // runInInjectionContext를 사용하여 주입 컨텍스트 내에서 함수 실행
      this.injector.runInContext(() => {
        const logger = inject(LoggerService);
        logger.log(message);
      });
    }, 1000);
  }
}

어? 코드가 똑같아 보이나요? 맞습니다. Injector.runInContext()는 사실상 runInInjectionContext의 초기 버전이었고, Angular 팀은 이를 Injector의 핵심 기능으로 통합했습니다. 만약 Injector.runInContext()를 이미 사용하고 있었다면, 사실상 여러분은 이미 새로운 패턴을 사용하고 있었던 것입니다!

하지만 runInInjectionContext의 진정한 강력함은 재사용 가능한 함수를 만들 때 드러납니다.

마이그레이션 예제: 커스텀 RxJS 오퍼레이터

가장 흔한 마이그레이션 사례 중 하나는 커스텀 RxJS 오퍼레이터를 만드는 것입니다. API 호출 결과를 로깅하는 커스텀 오퍼레이터를 예로 들어보겠습니다.

기존 방식

// old-custom-operator.ts
function tapLog<T>(injector: Injector): MonoTypeOperatorFunction<T> {
  return tap({
    next: (value) => {
      injector.runInContext(() => {
        const logger = inject(LoggerService);
        logger.log(`Value received: ${JSON.stringify(value)}`);
      });
    },
    error: (err) => {
      injector.runInContext(() => {
        const logger = inject(LoggerService);
        logger.log(`Error occurred: ${err.message}`);
      });
    },
  });
}

// 사용 예시
export class MyComponent {
  private injector = inject(Injector);

  data$ = this.myService.getData().pipe(
    tapLog(this.injector) // injector를 직접 전달해야 함
  );
}

이 방식의 단점은 오퍼레이터를 사용할 때마다 Injector 인스턴스를 명시적으로 전달해야 한다는 것입니다.

새로운 방식: runInInjectionContext 활용

runInInjectionContext를 사용하면 Injector를 캡처하여 훨씬 더 깔끔한 팩토리 함수를 만들 수 있습니다.

// new-custom-operator.ts
import { assertInInjectionContext, inject, Injector } from '@angular/core';
import { tap, MonoTypeOperatorFunction } from 'rxjs';
import { LoggerService } from './logger.service';

export function tapLog<T>(): MonoTypeOperatorFunction<T> {
  // 1. 이 함수가 주입 컨텍스트 내에서 호출되었는지 확인합니다.
  assertInInjectionContext(tapLog);

  // 2. 현재 컨텍스트의 Injector를 캡처합니다.
  const injector = inject(Injector);

  return tap({
    next: (value) => {
      // 3. 캡처된 injector를 사용하여 컨텍스트 내에서 로직을 실행합니다.
      injector.runInContext(() => {
        const logger = inject(LoggerService);
        logger.log(`Value received: ${JSON.stringify(value)}`);
      });
    },
    error: (err) => {
      injector.runInContext(() => {
        const logger = inject(LoggerService);
        logger.log(`Error occurred: ${err.message}`);
      });
    },
  });
}

// 사용 예시
export class MyComponent {
  // injector를 전달할 필요가 없습니다!
  data$ = this.myService.getData().pipe(tapLog());
}

assertInInjectionContexttapLog 함수가 constructor와 같이 주입 컨텍스트가 보장된 곳에서 호출되도록 강제하여 런타임 에러를 방지하는 안전장치입니다.

결론

runInInjectionContext는 Angular의 의존성 주입 시스템을 더욱 유연하고 강력하게 만들어주는 중요한 도구입니다. 기존의 Injector.runInContext()를 사용하던 코드를 리팩토링하거나, RxJS 오퍼레이터, 비동기 콜백 등에서 의존성을 주입해야 할 때 runInInjectionContext를 활용하여 더 깨끗하고 재사용 가능한 코드를 작성해 보세요.

이제 여러분의 코드베이스에서 Injector.runInContext를 검색하여 이 새로운 패턴으로 점진적으로 마이그레이션을 시작해 보시는 것을 추천합니다.

profile
교육하고 책 쓰는 개발자

0개의 댓글