Use Cases (업데이트: 2026. 6. 2.)

Claude Code로 Angular 개발하기: CLI, Standalone, Forms, HTTP, 테스트 실전

Angular CLI, standalone component, Reactive Forms, HttpClient, unit/E2E 테스트까지 Claude Code로 진행하는 실무 흐름.

Claude Code로 Angular 개발하기: CLI, Standalone, Forms, HTTP, 테스트 실전

Angular 프로젝트에서 Claude Code를 쓸 때 핵심은 “컴포넌트 하나를 멋지게 생성”하는 것이 아닙니다. 실제 업무에서는 Angular CLI 규칙, standalone component, Reactive Forms, HttpClient service, unit test, E2E, 마지막 코드 리뷰까지 이어집니다. 이 흐름을 작게 나누면 Claude Code의 결과를 팀 코드베이스에 맞추기 쉬워집니다.

standalone component는 NgModule에 등록하지 않고도 필요한 imports를 컴포넌트가 직접 선언하는 방식입니다. Reactive Forms는 폼 값과 검증 규칙을 TypeScript에서 명시적으로 관리하는 방식입니다. HttpClient는 Angular가 제공하는 HTTP 통신 API입니다. 이 개념을 프롬프트에 넣어야 Claude Code가 template-driven form이나 오래된 구조로 돌아가지 않습니다.

공식 문서는 계속 확인하세요. 기준은 Angular CLI, Reactive forms, HttpClient, Angular testing, Claude Code workflows입니다. 함께 보면 좋은 글은 TypeScript 팁, 테스트 전략, CLAUDE.md 베스트 프랙티스입니다.

먼저 작업 경계를 정한다

Claude Code에는 harness, 즉 에이전트가 안전하게 움직일 작업 프레임이 필요합니다. 수정할 파일, 금지할 패턴, 실행할 명령, 리뷰 기준을 알려줘야 합니다.

이 Angular 프로젝트를 먼저 읽어 주세요. package.json, angular.json, src/app을 확인하고
standalone component 사용 여부, 테스트 러너, HttpClient provider 위치,
명명 규칙을 요약하세요. 아직 파일은 수정하지 마세요.
영역Claude Code에 맡길 일사람이 결정할 일
Angular CLI생성 명령 후보, 파일 위치, 의존성 확인라우팅 소유권, 팀 네이밍
Componentimports, 템플릿, signal, 이벤트 처리UX 문구, 접근성, 릴리스 위험
Reactive FormsFormGroup, validator, submit 상태업무 검증 규칙
Service / HTTPAPI 타입, HttpClient 메서드, 테스트 더블API 계약, 인증, 에러 정책
테스트unit/E2E 시나리오, 회귀 테스트커버리지 목표, 머지 판단
flowchart LR
  P[Prompt] --> C[Angular CLI]
  C --> A[Standalone component]
  A --> F[Reactive Forms]
  A --> S[Ticket service]
  S --> H[HttpClient]
  F --> U[Unit tests]
  A --> E[E2E tests]
  U --> R[Review]
  E --> R

Angular CLI로 기준선을 만든다

새 검증 프로젝트라면 CLI로 시작합니다. 기존 저장소에서는 이 명령을 그대로 쓰기보다 Claude Code가 현재 설정을 먼저 읽게 해야 합니다.

npm install -g @angular/cli
ng new support-desk-angular --standalone --routing --style css
cd support-desk-angular
ng generate component features/tickets/ticket-intake --standalone
ng generate service data/ticket
ng test

수정 전 ng test가 통과하는지 확인해야 합니다. 실패한 상태에서 작업을 시작하면 Claude Code가 기존 실패와 새 실패를 구분하기 어렵습니다. 여러 사람이 동시에 작업한다면 “이 feature 폴더만 수정하고, 다른 미커밋 변경은 되돌리지 말라”는 조건도 넣습니다.

UI보다 service를 먼저 만든다

지원 티켓 등록 화면을 예로 들면, 먼저 API 타입과 service를 고정하는 것이 좋습니다. src/app/data/ticket.service.ts는 다음처럼 작성할 수 있습니다.

import { HttpClient } from '@angular/common/http';
import { inject, Injectable } from '@angular/core';
import { Observable } from 'rxjs';

export type TicketPriority = 'low' | 'medium' | 'high';

export interface TicketDraft {
  title: string;
  email: string;
  priority: TicketPriority;
  message: string;
}

export interface Ticket extends TicketDraft {
  id: string;
  createdAt: string;
}

@Injectable({ providedIn: 'root' })
export class TicketService {
  private readonly http = inject(HttpClient);
  private readonly baseUrl = '/api/tickets';

  listTickets(): Observable<Ticket[]> {
    return this.http.get<Ticket[]>(this.baseUrl);
  }

  createTicket(draft: TicketDraft): Observable<Ticket> {
    return this.http.post<Ticket>(this.baseUrl, draft);
  }
}

standalone 앱에서는 app.config.tsprovideHttpClient()를 둡니다.

import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideHttpClient } from '@angular/common/http';
import { routes } from './app.routes';

export const appConfig: ApplicationConfig = {
  providers: [provideRouter(routes), provideHttpClient()],
};

실패 사례는 service가 화면 문구까지 갖는 경우입니다. API 통신, 타입, endpoint는 service의 책임이지만 “저장에 실패했습니다” 같은 문구는 component가 맡아야 다국어와 화면별 표현을 관리하기 쉽습니다.

Reactive Forms로 standalone component를 만든다

아래 컴포넌트는 Reactive Forms와 signal을 함께 사용합니다. Claude Code에는 “ngModel을 쓰지 말라”고 분명히 적습니다.

import { Component, computed, inject, signal } from '@angular/core';
import { ReactiveFormsModule, FormControl, FormGroup, Validators } from '@angular/forms';
import { finalize } from 'rxjs';
import { Ticket, TicketService, TicketPriority } from '../../../data/ticket.service';

@Component({
  selector: 'app-ticket-intake',
  standalone: true,
  imports: [ReactiveFormsModule],
  template: `
    <form [formGroup]="form" (ngSubmit)="submit()" aria-label="Support ticket form">
      <label>Title <input type="text" formControlName="title" /></label>
      <label>Email <input type="email" formControlName="email" /></label>
      <label>
        Priority
        <select formControlName="priority">
          <option value="low">Low</option>
          <option value="medium">Medium</option>
          <option value="high">High</option>
        </select>
      </label>
      <label>Message <textarea formControlName="message"></textarea></label>

      @if (form.hasError('submit')) {
        <p role="alert">Ticket could not be saved. Please try again.</p>
      }
      @if (savedTicket(); as ticket) {
        <p role="status">Saved ticket {{ ticket.id }}</p>
      }
      <button type="submit" [disabled]="isSubmitDisabled()">
        {{ saving() ? 'Saving...' : 'Create ticket' }}
      </button>
    </form>
  `,
})
export class TicketIntakeComponent {
  private readonly ticketService = inject(TicketService);
  readonly saving = signal(false);
  readonly savedTicket = signal<Ticket | null>(null);

  readonly form = new FormGroup({
    title: new FormControl('', { nonNullable: true, validators: [Validators.required, Validators.minLength(5)] }),
    email: new FormControl('', { nonNullable: true, validators: [Validators.required, Validators.email] }),
    priority: new FormControl<TicketPriority>('medium', { nonNullable: true }),
    message: new FormControl('', { nonNullable: true, validators: [Validators.required, Validators.minLength(20)] }),
  });

  readonly isSubmitDisabled = computed(() => this.form.invalid || this.saving());

  submit(): void {
    this.form.markAllAsTouched();
    this.form.setErrors(null);
    if (this.form.invalid) return;

    this.saving.set(true);
    this.ticketService.createTicket(this.form.getRawValue()).pipe(
      finalize(() => this.saving.set(false)),
    ).subscribe({
      next: (ticket) => {
        this.savedTicket.set(ticket);
        this.form.reset({ title: '', email: '', priority: 'medium', message: '' });
      },
      error: () => this.form.setErrors({ submit: true }),
    });
  }
}

주의할 점은 세 가지입니다. ngModelformControlName을 섞지 않습니다. 저장 중 버튼을 비활성화합니다. nullable 값이 섞일 수 있는 form.value 대신 non-nullable control과 getRawValue()를 사용합니다.

실제 유스케이스 네 가지

첫째, 관리자 입력 폼입니다. 필드, validation, 저장 중 상태, 성공 표시, 실패 표시를 구체적으로 써야 합니다. “예쁘게 만들어”는 리뷰 가능한 요구사항이 아닙니다.

둘째, 컴포넌트 안에 흩어진 HTTP 호출을 service로 옮기는 작업입니다. Claude Code에 타입 정의, HttpClient 이동, UI 문구 분리, HTTP 테스트 추가를 함께 요청합니다.

셋째, 버그 수정과 회귀 테스트입니다. 예를 들어 실패 후 버튼이 계속 disabled라면 재현 절차, 기대 결과, 테스트 명령을 주고 실패 경로 테스트까지 추가하게 합니다.

넷째, NgModule 중심 코드에서 standalone으로 점진적으로 옮기는 작업입니다. 전체 앱을 한 번에 바꾸지 말고 feature 단위로 제한하고 routing과 provider는 유지합니다.

unit test, E2E, 리뷰 프롬프트

HttpClient 테스트는 실제 네트워크로 나가면 안 됩니다. Angular testing backend로 요청을 검증합니다.

import { TestBed } from '@angular/core/testing';
import { provideHttpClient } from '@angular/common/http';
import { provideHttpClientTesting, HttpTestingController } from '@angular/common/http/testing';
import { TicketService } from './ticket.service';

describe('TicketService', () => {
  let service: TicketService;
  let http: HttpTestingController;

  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [TicketService, provideHttpClient(), provideHttpClientTesting()],
    });
    service = TestBed.inject(TicketService);
    http = TestBed.inject(HttpTestingController);
  });

  afterEach(() => http.verify());

  it('creates a ticket through the API', () => {
    const draft = {
      title: 'Billing export is stuck',
      email: 'ops@example.com',
      priority: 'high' as const,
      message: 'The monthly billing export has not finished for two hours.',
    };

    service.createTicket(draft).subscribe((ticket) => expect(ticket.id).toBe('T-100'));
    const req = http.expectOne('/api/tickets');
    expect(req.request.method).toBe('POST');
    expect(req.request.body).toEqual(draft);
    req.flush({ ...draft, id: 'T-100', createdAt: '2026-06-02T09:00:00.000Z' });
  });
});

E2E는 프로젝트에 target이 있으면 ng e2e, Playwright를 쓰면 npx playwright test로 실행합니다. 핵심은 브라우저에서 사용자의 입력, 제출, 성공 표시가 이어지는지 확인하는 것입니다.

이번 Angular 변경을 비판적으로 리뷰해 주세요.
standalone imports/provider, Reactive Forms nullable 값, 중복 제출,
service와 UI 책임 분리, HttpClient 테스트의 실제 네트워크 호출 여부,
unit/E2E 실패 경로, any 사용, 접근성 문제를 확인해 주세요.
파일명과 줄 번호를 포함해 우선순위대로 지적해 주세요.

결론과 실제 적용 결과

Claude Code는 Angular 작업을 빠르게 만들 수 있지만, 품질은 프롬프트의 경계와 검증에서 나옵니다. 추천 흐름은 조사, service, component, unit test, E2E, 리뷰입니다.

작은 Angular 검증 프로젝트에서 테스트해 보니, “폼을 만들어 줘”라고만 요청했을 때는 ngModel 혼합, 실패 경로 테스트 누락, service의 UI 문구가 생겼습니다. 반대로 Reactive Forms, provideHttpClient, HttpTestingController, any 금지, 실패 시 버튼 상태 복구를 명시하자 수정 범위가 훨씬 작아졌습니다. 팀 도입이 필요하다면 이 규칙을 CLAUDE.md에 넣고 Claude Code 교육 및 도입 상담으로 반복 가능한 리뷰 흐름을 설계하는 것이 현실적입니다.

#Claude Code #Angular #TypeScript #frontend #enterprise
무료

무료 PDF: Claude Code 치트시트

이메일을 입력하면 명령, 리뷰 습관, 안전한 워크플로를 정리한 PDF를 받을 수 있습니다.

개인정보를 안전하게 관리하며 스팸을 보내지 않습니다.

Masa

작성자 소개

Masa

Claude Code 실무 워크플로와 팀 도입을 검증하는 엔지니어입니다.