Angular v16부터 도입된 runInInjectionContext
는 의존성 주입(DI) 컨텍스트가 없는 곳에서 DI 컨텍스트가 필요한 함수를 실행할 수 있게 해주는 강력한 API입니다. 이 가이드에서는 runInInjectionContext
가 필요한 이유를 알아보고, 기존 코드를 새로운 방식으로 마이그레이션하는 방법을 안내합니다.
runInInjectionContext
가 필요한가요?Angular의 의존성 주입 시스템은 기본적으로 클래스의 constructor
내부와 같이 명확한 "주입 컨텍스트" 내에서 동기적으로 작동합니다. 하지만 다음과 같은 상황에서는 주입 컨텍스트 외부에서 서비스를 주입해야 할 필요가 있습니다.
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
runInInjectionContext
는 Injector
인스턴스의 메서드로 제공되어 훨씬 더 자연스럽게 코드를 작성할 수 있습니다.
위 예제를 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 오퍼레이터를 만드는 것입니다. 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());
}
assertInInjectionContext
는 tapLog
함수가 constructor
와 같이 주입 컨텍스트가 보장된 곳에서 호출되도록 강제하여 런타임 에러를 방지하는 안전장치입니다.
runInInjectionContext
는 Angular의 의존성 주입 시스템을 더욱 유연하고 강력하게 만들어주는 중요한 도구입니다. 기존의 Injector.runInContext()
를 사용하던 코드를 리팩토링하거나, RxJS 오퍼레이터, 비동기 콜백 등에서 의존성을 주입해야 할 때 runInInjectionContext
를 활용하여 더 깨끗하고 재사용 가능한 코드를 작성해 보세요.
이제 여러분의 코드베이스에서 Injector.runInContext
를 검색하여 이 새로운 패턴으로 점진적으로 마이그레이션을 시작해 보시는 것을 추천합니다.