다크 모드
Appendix C. 실습 환경·런타임 에러 트러블슈팅 FAQ 14종
"개발자가 가장 지치고 포기하고 싶을 때는 코드가 어려울 때가 아니라, 환경 설정과 사소한 에러 때문에 화면이 안 뜰 때이다. 사내 실습 시 자주 마주치는 환경 이슈 5가지(FAQ 1~5)와, Angular 22 + TypeScript 6.0 특유의 컴파일·런타임 에러 9가지(FAQ 6~14)의 원인과 해결 코드를 총정리한다."
FAQ 1. npm start (ng serve) 실행 시 4200번 포트가 이미 사용 중이라며 충돌해요
[증상]
터미널에 Port 4200 is already in use. Would you like to use a different port? 메시지가 뜨거나 서버가 기동되지 않습니다.
[원인]
이전에 실행했던 Angular 개발 서버 프로세스가 백그라운드에 종료되지 않고 남아 있거나, 사내 다른 프로세스가 4200번 포트를 점유하고 있습니다.
[해결 방법]
Windows PowerShell에서 4200번 포트를 Listen 중인 프로세스만 찾아 즉시 종료합니다:
powershell
Get-NetTCPConnection -LocalPort 4200 -State Listen |
Select-Object -ExpandProperty OwningProcess -Unique |
ForEach-Object { Stop-Process -Id $_ -Force }macOS / Linux의 경우:
bash
lsof -ti:4200 | xargs kill -9FAQ 2. VS Code에서 @for, @if나 input.required()에 빨간 줄이 그어져요
[증상]
빌드(npm run build)는 정상 통과하는데, VS Code 에디터 안에서 @for (post of posts(); track post.id) 구문이나 템플릿의 신호 호출에 빨간 줄이 표시됩니다.
[원인]
VS Code에 공식 Angular Language Service 확장이 설치되어 있지 않거나, 구버전 확장이 Angular 22의 Control Flow 및 Signal Input 문법을 인식하지 못해 발생합니다.
[해결 방법]
- VS Code 확장 탭(
Ctrl + Shift + X)에서Angular Language Service(Angular.ng-template) 최신 버전을 설치합니다. Ctrl + Shift + P→Angular: Restart Angular Language Server또는 **Developer: Reload Window**를 실행합니다.
FAQ 3. Spring Boot 실행 시 release version 21 not supported / class file version 61.0 에러가 나요
[증상]
backend/ 폴더에서 ./mvnw spring-boot:run 실행 시 자바 버전 불일치로 빌드가 중단됩니다.
[원인]
본 프로젝트의 Spring Boot 3 백엔드는 Java 21 LTS (클래스 파일 버전 65.0) 기준입니다. 터미널의 JAVA_HOME이 사내 레거시용 JDK 8(52.0)이나 JDK 11(55.0), JDK 17(61.0)을 가리키고 있을 때 발생합니다.
[해결 방법]
JDK 21을 설치하고 현재 터미널 세션의 JAVA_HOME을 JDK 21 경로로 지정합니다:
powershell
# Windows PowerShell
$env:JAVA_HOME = "C:\Program Files\Eclipse Adoptium\jdk-21.0.5.11-hotspot"
$env:Path = "$env:JAVA_HOME\bin;" + $env:Path
.\mvnw.cmd spring-boot:runFAQ 4. NG0100: ExpressionChangedAfterItHasBeenCheckedError 에러가 콘솔에 찍혀요
[증상]
개발 모드 콘솔에 RuntimeError: NG0100: ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. 에러가 발생합니다.
[원인]
과거 방식처럼 일반 클래스 멤버 변수(isLoading = false)를 쓰고, 자식 컴포넌트의 ngAfterViewInit 등 렌더링 도중 부모의 변수를 동기적으로 바꿔버려 변경 감지 전후 값이 달라졌기 때문입니다.
[해결 방법]
모든 반응형 상태를 일반 변수가 아닌 **Angular Signals (signal(), computed())**로 전환하십시오. Signals는 단방향 의존성 그래프를 통해 더티 마킹을 관리하므로 NG0100 에러를 원천 예방합니다:
typescript
// ❌ 지양: 일반 프로퍼티를 렌더링 훅 도중 변경
isLoading = false;
// ✅ 권장: Signals + OnPush 조합 사용
readonly isLoading = signal(false);FAQ 5. NullInjectorError: R3InjectorError(EnvironmentInjector)[HttpClient -> HttpClient]: No provider for HttpClient!
[증상]
화면을 열거나 테스트를 실행할 때 No provider for HttpClient! 에러가 나며 컴포넌트가 생성되지 않습니다.
[원인]
Standalone 애플리케이션의 main.ts(또는 테스트의 TestBed.configureTestingModule)에서 provideHttpClient() 공급자를 등록하지 않았습니다.
[해결 방법]
main.ts의 providers 배열에 provideHttpClient()를 추가합니다:
typescript
// src/main.ts
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { authInterceptor } from './app/interceptors/auth.interceptor';
bootstrapApplication(AppComponent, {
providers: [
provideHttpClient(withInterceptors([authInterceptor]))
]
});FAQ 6. @for 블록에서 NG5002: @for loop must have a "track" expression 컴파일 에러가 나요
[증상]
템플릿에 @for (post of posts())라고 썼더니 빌드 시 track 표현식이 필수라는 컴파일 에러가 발생합니다.
[원인]
과거 *ngFor에서는 trackBy가 선택 사항이었으나, 초보자들이 이를 생략해 대량 DOM 재생성 성능 저하가 빈발하자 모던 Angular의 @for 문법은 track 절을 문법적으로 강제합니다.
[해결 방법]
반드시 고유 식별자(post.id)를 track 뒤에 명시합니다:
html
@for (post of posts(); track post.id) {
<tr><td>{{ post.title }}</td></tr>
}FAQ 7. 화면에 데이터 대신 [Signal: ...] 또는 [object Function]이 그대로 출력돼요
[증상]
템플릿에 게시글 제목이나 개수를 출력했는데 실제 값 대신 [Signal: 0]이나 함수 본문이 문자열로 찍힙니다.
[원인]
Signals는 Getter 함수인데 템플릿에서 괄호 ()를 빼먹고 {{ store.totalElements }}처럼 함수 참조 자체를 출력했기 때문입니다.
[해결 방법]
템플릿과 스크립트 어디서든 신호 값을 읽을 때는 반드시 ()를 붙여 호출합니다:
html
<!-- ❌ 잘못된 표기 -->
<span>총 {{ store.totalElements }}건</span>
<!-- ✅ 올바른 표기 -->
<span>총 {{ store.totalElements() }}건</span>FAQ 8. NG0200: Circular dependency in DI detected for AuthStore
[증상]
애플리케이션 시작 또는 API 호출 순간 NG0200: Circular dependency in DI detected 에러가 발생하며 멈춥니다.
[원인]
예를 들어 AuthStore가 생성 시점에 HttpClient를 주입받아 호출하고, HttpClient의 인터셉터가 다시 AuthStore를 주입받는 구조에서 서로의 초기화를 기다리는 순환 참조가 발생한 경우입니다.
[해결 방법]
- 11장~12장 설계처럼
AuthStore는 토큰 상태만 들고 있고, 토큰 갱신 HTTP 호출은authInterceptor또는 별도AuthApiService에서 수행하도록 책임을 분리합니다. - 또는 인터셉터에서
SKIP_AUTH_INTERCEPTOR컨텍스트 토큰을 사용해 인증 API가 자기 자신을 재호출하지 않게 차단합니다(12장 [코드 12-2]).
FAQ 9. 로컬 개발 중 백엔드 API 호출 시 CORS policy: No 'Access-Control-Allow-Origin' 에러 또는 200 OK인데 JSON 파싱 에러가 나요
[증상]
/api/posts를 호출했는데 CORS 에러가 나거나, 응답으로 JSON 대신 프론트엔드의 index.html(<!doctype html>...)이 돌아와 Unexpected token '<' 파싱 에러가 발생합니다.
[원인]
Angular 개발 서버(localhost:4200)에 /api 요청을 Spring Boot(localhost:8080)로 넘겨주는 리버스 프록시(proxy.conf.json)가 연결되지 않았습니다.
[해결 방법]
frontend/proxy.conf.json 파일을 만들고 angular.json의 serve.options.proxyConfig에 연결합니다:
json
// frontend/proxy.conf.json
{
"/api": {
"target": "http://localhost:8080",
"secure": false,
"changeOrigin": true
}
}그리고 ng serve --proxy-config proxy.conf.json으로 실행합니다.
FAQ 10. NG0203: effect() / inject() can only be called within an injection context
[증상]
버튼 클릭 핸들러나 ngOnInit(), 혹은 async/await의 await 아랫줄에서 inject()나 effect(), httpResource()를 호출했더니 NG0203 런타임 에러가 발생합니다.
[원인]
inject(), effect(), httpResource(), takeUntilDestroyed()는 Angular가 현재 어느 컴포넌트·서비스를 만들고 있는지 알고 있는 의존성 주입 컨텍스트(클래스 필드 초기화식 또는 constructor 동기 구간) 안에서만 호출할 수 있습니다.
[해결 방법]
inject()와effect()는 항상 클래스 필드 선언부 또는constructor()최상단 동기 구간에 작성합니다.- 부득이하게 메서드 안에서
effect()를 생성해야 한다면 생성자에서 미리 주입받은Injector를 옵션으로 넘깁니다:
typescript
export class MyComponent {
private readonly injector = inject(Injector);
startTracking(): void {
effect(() => {
console.log('추적 중:', this.count());
}, { injector: this.injector });
}
}FAQ 11. NG0912: Component ID generation collision detected 경고가 떠요
[증상]
콘솔에 두 컴포넌트의 selector와 구조가 같아 ID가 충돌했다는 NG0912 경고가 표시됩니다.
[원인]
8장·9장처럼 장별 스냅샷 컴포넌트(snapshots/board-app.ch08.component.ts 등)를 복사해 두면서 동일한 selector: 'app-board-app'을 가진 클래스 여러 개가 동시에 번들에 포함되었을 때 발생합니다.
[해결 방법]
실제 사용하는 컴포넌트 하나만 imports에 등록하거나, 스냅샷/연습용 컴포넌트는 selector: 'app-board-app-ch08'처럼 고유한 셀렉터 이름을 부여합니다.
FAQ 12. Vitest에서 input.required<T>() 신호를 가진 컴포넌트를 테스트할 때 NG0950 에러가 나요
[증상]
TestBed.createComponent(BoardTableComponent) 직후 fixture.detectChanges()를 호출했더니 NG0950: Input "items" is required but no value is available yet. 에러로 테스트가 실패합니다.
[원인]
input.required()로 선언된 필수 입력 신호에 값을 넣어주지 않은 채 첫 변경 감지(detectChanges())를 실행했기 때문입니다.
[해결 방법]
첫 fixture.detectChanges()를 호출하기 전에 fixture.componentRef.setInput()으로 필수 입력값을 먼저 주입합니다:
typescript
const fixture = TestBed.createComponent(BoardTableComponent);
fixture.componentRef.setInput('items', []);
fixture.componentRef.setInput('columns', []);
fixture.detectChanges(); // ✅ 이제 에러 없이 통과!FAQ 13. Angular 22 설치·빌드 시 Node.js version v20.x is not supported 에러가 나요
[증상]
npm start나 ng build 실행 시 Node.js 버전이나 TypeScript 버전이 낮다는 에러가 출력됩니다.
[원인]
Angular 22는 Node.js 20 지원을 공식 종료하고 Node.js 22 LTS 이상 및 TypeScript 6.0 이상을 필수로 요구합니다.
[해결 방법]
터미널에서 node -v를 확인하고 Node.js 22 LTS 버전으로 전환합니다:
bash
nvm install 22
nvm use 22
node -v # v22.x.x 확인FAQ 14. 값을 분명히 바꿨는데 화면이 전혀 갱신되지 않아요 (OnPush + 참조 동일성 함정)
[증상]
this._posts().push(newPost) 또는 this._pendingPostIds().add(1)을 실행했는데 테이블 화면이나 버튼 상태가 전혀 바뀌지 않습니다.
[원인]
Angular 22는 모든 신규 컴포넌트가 기본적으로 **ChangeDetectionStrategy.OnPush**로 동작하며, Signals는 **Object.is(이전참조, 새참조)**로 변경 여부를 판단합니다. 기존 배열이나 Set 내부를 직접 변경(Mutate)하면 배열/Set의 메모리 참조 주소가 그대로이므로 신호가 변경을 알리지 않습니다.
[해결 방법]
항상 .update()를 사용해 새로운 배열([...prev, newPost])이나 새로운 Set(new Set(prev)) 인스턴스를 반환하십시오(10장 [코드 10-1] 참고):
typescript
// ❌ 잘못된 방식: 기존 배열을 직접 변형 (참조가 같아 화면 갱신 안 됨!)
this._posts().push(newPost);
// ✅ 올바른 방식: 새 배열 참조를 생성하여 반환 (즉시 화면 갱신!)
this._posts.update((prev) => [newPost, ...prev]);