단일 HTML 파일의 각도

Angular는 일반적으로 빌드 단계가 있는 TypeScript 프로젝트입니다. 작은 페이지의 경우 CDN에서 가져오기 세 번과 함수 호출 한 번이면 단일 HTML 파일에서 실행할 수 있습니다.

영문 원문 보기

여기서 'Angular'는 angle.dev에 문서화되어 있고 Google 팀에서 유지관리하는 웹 프레임워크를 의미합니다.

일반적인 Angular 앱은 Angular CLI가 빌드하는 TypeScript 프로젝트입니다. 작은 것도 하나의 HTML 파일에서 실행됩니다. jsDelivr에서 세 개의 Angular 패키지를 가져오면 페이지가 열릴 때 브라우저가 템플릿을 컴파일합니다.

먼저 시도해 보세요. 아래 카운터는 설치나 빌드 없이 CDN에서 로드된 Angular 22.2.1입니다.

실행 예제직접 써 보고 코드를 복사하세요
링크로 공유하기
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Angular in one HTML file</title>
<style>
  body { margin: 0; padding: 24px; font-family: system-ui, sans-serif; background: #f4f5f7; }
  .card { max-width: 320px; padding: 18px 20px; border-radius: 12px; background: #fff; box-shadow: 0 6px 20px rgba(0, 0, 0, .1); }
  button { font: inherit; padding: 8px 16px; border: 0; border-radius: 8px; background: #c3002f; color: #fff; cursor: pointer; }
  p { margin: 0 0 12px; }
</style>
</head>
<body>
<!-- Angular replaces what is inside this tag once it starts -->
<app-root>Loading Angular...</app-root>

<script type="module">
  // The compiler must load first: it turns the template string into code in the browser
  import 'https://cdn.jsdelivr.net/npm/@angular/compiler@22.2.1/+esm';
  import { Component, signal } from 'https://cdn.jsdelivr.net/npm/@angular/core@22.2.1/+esm';
  import { bootstrapApplication } from 'https://cdn.jsdelivr.net/npm/@angular/platform-browser@22.2.1/+esm';

  class App {
    count = signal(0);  // a value the template reads by calling count()
  }

  // No TypeScript here, so Component() is called on the class instead of written as @Component
  Component({
    selector: 'app-root',
    template: `
      <div class="card">
        <p>You clicked <b>{{ count() }}</b> times.</p>
        <button (click)="count.set(count() + 1)">Click me</button>
      </div>`,
  })(App);

  bootstrapApplication(App);
</script>
</body>
</html>
하나의 파일에 완전한 Angular 페이지가 있습니다. 템플릿이나 클래스를 편집하면 예제가 다시 실행됩니다.

전체 앱은 하단의 모듈 스크립트입니다. 이 가이드의 나머지 부분에서는 이를 분해하여 가장 많이 사용할 템플릿 구문을 보여주고 실제 Angular 프로젝트로 이동할 때 변경되는 사항을 설명합니다.

페이지의 네 부분

모든 단일 파일 Angular 페이지에는 이 순서대로 동일한 네 부분이 있습니다.

자리표시자 태그, 가져오기 3개, 템플릿이 포함된 클래스, 이를 시작하는 호출 1개.
자리표시자 태그, 가져오기 3개, 템플릿이 포함된 클래스, 이를 시작하는 호출 1개.
  1. 자리 표시자 태그. 내부에 일부 텍스트와 함께 <app-root>를 본문에 넣습니다. Angular가 시작될 때까지 텍스트가 표시되므로 계속 표시되면 Angular가 시작되지 않은 것입니다.
  2. 모듈 스크립트에는 세 가지 가져오기가 있습니다. @angular/compiler가 먼저 오고 그 다음 @angular/core 및 @angular/platform-browser가 옵니다. 스크립트 태그에 type="module"가 필요하거나 import에 구문 오류가 있습니다.
  3. 클래스와 해당 템플릿. 상태는 클래스에 신호로 존재합니다. Component()는 자리 표시자 태그와 일치해야 하는 selector와 template 문자열을 가져옵니다.
  4. 한 번만 호출하면 시작됩니다. bootstrapApplication(App)는 태그를 찾아 그 안에 템플릿을 렌더링합니다.

모든 Angular 패키지를 동일한 버전 번호로 고정합니다. @angular/core 22.2.1 패키지에는 정확한 버전인 @angular/compiler 22.2.1이 피어 종속성으로 나열되어 있습니다.

빌드 없이 실행되는 이유

Angular에는 템플릿을 컴파일하는 두 가지 방법이 있습니다. AOT(사전)는 빌드 시 이를 컴파일하며, 이는 ng build 및 ng serve의 기본값입니다. JIT(적시)는 페이지가 실행되는 동안 브라우저에서 이를 컴파일합니다.

단일 파일이 브라우저에서 컴파일됩니다. CLI 프로젝트는 누군가가 페이지를 열기 전에 한 번 컴파일됩니다.
단일 파일이 브라우저에서 컴파일됩니다. CLI 프로젝트는 누군가가 페이지를 열기 전에 한 번 컴파일됩니다.

HTML 파일 하나로 실행할 때는 빌드 과정이 없으므로 JIT 방식을 사용합니다. 그래서 @angular/compiler를 먼저 가져옵니다. 이 모듈이 없으면 Angular는 템플릿 문자열을 코드로 변환할 수 없습니다.

Angular 문서에서는 컴파일러가 Angular의 큰 부분을 차지하므로 이를 생략한 AOT 빌드는 훨씬 적은 코드를 제공합니다.

zone.js 스크립트도 없습니다. Angular 문서에서는 zone.js 없는 변경 감지가 v21의 기본값이라고 말합니다. 템플릿이 읽는 신호를 업데이트하거나 템플릿에 바인딩된 리스너를 실행하면 Angular가 뷰를 새로 고치도록 지시합니다.

가장 많이 사용할 템플릿 구문

템플릿은 몇 가지 추가 사항이 포함된 HTML입니다. 이 예에서는 그 중 대부분을 사용합니다. 목록을 필터링하려면 상자에 입력하세요.

실행 예제직접 써 보고 코드를 복사하세요
링크로 공유하기
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Angular template syntax</title>
<style>
  body { margin: 0; padding: 20px; font-family: system-ui, sans-serif; background: #f4f5f7; }
  .card { max-width: 360px; padding: 16px 18px; border-radius: 12px; background: #fff; box-shadow: 0 6px 20px rgba(0, 0, 0, .1); }
  input { font: inherit; padding: 7px 9px; border: 1px solid #c9ced6; border-radius: 8px; width: 100%; box-sizing: border-box; }
  ul { padding-left: 20px; }
  li.hit { background: #fff3c4; }
  .note { color: #6b7280; font-size: 14px; }
</style>
</head>
<body>
<app-root>Loading Angular...</app-root>

<script type="module">
  import 'https://cdn.jsdelivr.net/npm/@angular/compiler@22.2.1/+esm';
  import { Component, signal, computed } from 'https://cdn.jsdelivr.net/npm/@angular/core@22.2.1/+esm';
  import { bootstrapApplication } from 'https://cdn.jsdelivr.net/npm/@angular/platform-browser@22.2.1/+esm';

  class App {
    fruits = ['Apple', 'Banana', 'Cherry', 'Grape', 'Mango', 'Pear'];
    query = signal('');
    // computed() re-runs only when query changes
    matches = computed(() =>
      this.fruits.filter(f => f.toLowerCase().includes(this.query().toLowerCase())));
  }

  Component({
    selector: 'app-root',
    template: `
      <div class="card">
        <!-- [value] sets a property; (input) listens to an event, $event is the DOM event -->
        <input placeholder="Filter fruit" [value]="query()" (input)="query.set($event.target.value)">

        <!-- {{ }} prints a value -->
        <p class="note">{{ matches().length }} of {{ fruits.length }} shown</p>

        <ul>
          <!-- @for needs track so Angular can reuse list items -->
          @for (f of matches(); track f) {
            <li [class.hit]="query() !== ''">{{ f }}</li>
          } @empty {
            <li>No fruit matches "{{ query() }}"</li>
          }
        </ul>

        @if (query()) {
          <button (click)="query.set('')">Clear filter</button>
        }
      </div>`,
  })(App);

  bootstrapApplication(App);
</script>
</body>
</html>
보간, 속성 바인딩, 이벤트 바인딩, @for와 @empty 및 @if를 하나의 작은 필터에 담았습니다.
구문 기능 예제에서는
{{ value }} 값을 텍스트로 인쇄합니다. "6/6 표시" 라인
[property]="expr" DOM 속성을 설정합니다. 입력의 [value]
[class.name]="expr" 값이 진실일 때 클래스를 추가합니다. 각 항목에 대한 [class.hit]
(event)="statement" 이벤트가 발생하면 코드를 실행합니다. (input) 및 (click)
$event 리스너 내부의 DOM 이벤트 입력된 텍스트를 읽습니다.
@if (cond) { } true인 경우에만 블록을 표시합니다. 지우기 버튼
@for (x of list; track x) { } 각 항목에 대해 블록을 반복합니다. 과일 목록
@empty { } @for 목록이 비어 있을 때 표시됩니다. "과일 일치 없음" 라인

클래스의 두 부분이 그것을 주도합니다. query는 쓰기 가능한 신호입니다. query()로 읽고 query.set(...)로 변경하세요. matches는 computed() 신호로, query가 변경되면 다시 계산됩니다.

query = signal('');
matches = computed(() =>
  this.fruits.filter(f => f.toLowerCase().includes(this.query().toLowerCase())));

@for에는 track가 필요합니다. 그대로 두면 템플릿이 컴파일되지 않아 페이지가 로딩 텍스트에 그대로 유지됩니다.

TypeScript 없이 구성 요소 작성

CLI 프로젝트에서 구성 요소는 위에 @Component가 작성된 TypeScript 클래스이며 템플릿은 종종 자체 파일에 존재합니다. 일반 .html 파일을 실행하는 브라우저에는 TypeScript 단계가 없고 로드할 두 번째 파일이 없습니다.

TypeScript에서는 데코레이터로, 일반 JavaScript에서는 함수 호출로 작성된 동일한 구성 요소 옵션입니다.
TypeScript에서는 데코레이터로, 일반 JavaScript에서는 함수 호출로 작성된 동일한 구성 요소 옵션입니다.

따라서 단일 파일은 동일한 옵션으로 Component()를 호출하고 이를 클래스에 전달합니다.

class App {
  count = signal(0);
}

Component({
  selector: 'app-root',
  template: `<button (click)="count.set(count() + 1)">{{ count() }}</button>`,
})(App);

templateUrl가 아니라 백틱 문자열과 함께 template를 사용하세요. 백틱을 사용하면 템플릿이 여러 줄에 걸쳐 표시됩니다. 나중에 코드를 CLI 프로젝트로 이동하면 옵션이 그대로 유지됩니다.

완성된 예: 포장 목록

이것은 양식, 확인란 및 제거를 추가합니다. Enter 또는 버튼을 사용하여 항목을 추가하고 항목을 해제한 다음 십자가로 제거합니다.

실행 예제직접 써 보고 코드를 복사하세요
링크로 공유하기
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Packing list in Angular</title>
<style>
  body { margin: 0; padding: 20px; font-family: system-ui, sans-serif; background: #f4f5f7; }
  .card { max-width: 380px; padding: 16px 18px; border-radius: 12px; background: #fff; box-shadow: 0 6px 20px rgba(0, 0, 0, .1); }
  h2 { margin: 0 0 4px; font-size: 20px; }
  .note { margin: 0 0 12px; color: #6b7280; font-size: 14px; }
  form { display: flex; gap: 8px; }
  input[type=text] { flex: 1; min-width: 0; font: inherit; padding: 7px 9px; border: 1px solid #c9ced6; border-radius: 8px; }
  button { font: inherit; padding: 7px 12px; border: 0; border-radius: 8px; background: #c3002f; color: #fff; cursor: pointer; }
  ul { list-style: none; padding: 0; margin: 12px 0 0; }
  li { display: flex; align-items: center; gap: 8px; padding: 7px 0; border-bottom: 1px solid #eceef2; }
  li label { flex: 1; }
  li.done label { color: #9ca3af; text-decoration: line-through; }
  .x { background: none; color: #9ca3af; padding: 2px 8px; }
  .all { margin-top: 12px; color: #0f5132; font-weight: 600; }
</style>
</head>
<body>
<app-root>Loading Angular...</app-root>

<script type="module">
  import 'https://cdn.jsdelivr.net/npm/@angular/compiler@22.2.1/+esm';
  import { Component, signal, computed } from 'https://cdn.jsdelivr.net/npm/@angular/core@22.2.1/+esm';
  import { bootstrapApplication } from 'https://cdn.jsdelivr.net/npm/@angular/platform-browser@22.2.1/+esm';

  let nextId = 4;

  class App {
    items = signal([
      { id: 1, name: 'Passport', done: true },
      { id: 2, name: 'Charger', done: false },
      { id: 3, name: 'Rain jacket', done: false },
    ]);
    left = computed(() => this.items().filter(i => !i.done).length);

    add(event, input) {
      event.preventDefault();  // keep the form from reloading the page
      const name = input.value.trim();
      if (!name) return;
      this.items.update(list => [...list, { id: nextId++, name, done: false }]);
      input.value = '';
    }
    toggle(id) {
      // replace the list (not mutate it) so the signal sees a new value
      this.items.update(list => list.map(i => i.id === id ? { ...i, done: !i.done } : i));
    }
    remove(id) {
      this.items.update(list => list.filter(i => i.id !== id));
    }
  }

  Component({
    selector: 'app-root',
    template: `
      <div class="card">
        <h2>Packing list</h2>
        <p class="note">{{ left() }} of {{ items().length }} still to pack</p>

        <!-- #box names the input so the template can pass it to add() -->
        <form (submit)="add($event, box)">
          <input #box type="text" placeholder="Add an item" aria-label="New item">
          <button>Add</button>
        </form>

        <ul>
          @for (item of items(); track item.id) {
            <li [class.done]="item.done">
              <input type="checkbox" [id]="'i' + item.id" [checked]="item.done" (change)="toggle(item.id)">
              <label [for]="'i' + item.id">{{ item.name }}</label>
              <button class="x" (click)="remove(item.id)" aria-label="Remove">✕</button>
            </li>
          }
        </ul>

        @if (items().length && left() === 0) {
          <p class="all">All packed.</p>
        }
      </div>`,
  })(App);

  bootstrapApplication(App);
</script>
</body>
</html>
하나의 구성 요소: 항목을 추가하는 양식, 항목을 선택하는 확인란 및 유지되는 개수입니다.
  • 양식은 페이지를 다시 로드하지 않습니다. (submit)="add($event, box)"가 이벤트를 전달하고 add()가 event.preventDefault()를 호출합니다. Angular 문서에서는 명시적으로 호출하는 것을 권장합니다.
  • #box는 입력 이름을 지정하므로 템플릿은 해당 값을 읽고 지울 add()에 요소를 전달할 수 있습니다.
  • 변경할 때마다 새로운 배열이 만들어집니다. add(), toggle() 및 remove()는 모두 items.update()를 호출하고 새 목록을 반환합니다. 신호가 새로운 값을 얻으므로 left 카운트가 다시 계산됩니다.
  • track item.id 항목이 중간에서 제거될 때 Angular가 올바른 행을 유지할 수 있습니다.

일반 JavaScript로 된 할 일 목록을 구축한 경우 두 가지를 비교하십시오. Angular 버전에는 innerHTML가 없으며 업데이트할 행을 찾는 코드도 없습니다. 템플릿은 모든 상태에 대한 페이지를 설명합니다.

Angular CLI 프로젝트로 이동

단일 파일은 학습, 프로토타입 또는 작은 도구에 적합합니다. 구성 요소, 라우팅 및 테스트가 많은 앱의 경우 Angular 문서는 Node.js가 필요한 CLI로 시작합니다.

npm install -g @angular/cli
ng new my-app
cd my-app
npm start

개발 서버는 http://localhost:4200에서 실행됩니다.

ng build는 TypeScript와 템플릿을 미리 컴파일하고 기본적으로 결과를 dist/my-app에 씁니다. 해당 출력은 하나의 HTML 페이지가 아닌 파일 폴더이므로 구축된 프런트 엔드처럼 호스팅됩니다.

빌드된 프런트 엔드 호스팅은 기본 경로와 거기에서 발생하는 새로 고침 오류를 다룹니다.

아무것도 설치하지 않고 CLI 스타일 코드를 사용해 보려면 Angular 사이트에 브라우저 놀이터가 있습니다.

Angular 또는 AngularJS?

단일 angular.js 스크립트와 함께 ng-app 및 ng-model를 사용하는 이전 튜토리얼은 AngularJS입니다. 이는 별도의 이전 프레임워크이며 해당 사이트에는 2022년 1월에 지원이 종료되었다고 나와 있습니다.

AngularJS 각도
스타트업 요소의 ng-app 속성 bootstrapApplication(App)
템플릿 루프 ng-repeat @for
문서 anglejs.org 각도.dev

한 코드는 다른 코드에서 실행되지 않습니다. 이 페이지의 모든 내용은 Angular입니다.

작동하지 않을 때

먼저 브라우저 콘솔을 엽니다. Angular가 시작되지 않으면 오류가 발생하고 페이지에 여전히 로딩 텍스트가 표시됩니다.

당신이 보는 것 원인 수정
"로드 중"에서 멈춤, 콘솔에 컴파일러를 사용할 수 없다고 표시됨 @angular/compiler를 가져오지 않음 코어 이전에 먼저 가져오기
"로드 중"에서 멈춤, "모듈 외부에서 import 문을 사용할 수 없습니다" 스크립트 태그에 유형이 없습니다. <script type="module"> 사용
"로드 중"에서 멈춤, 오류 NG05104 선택기와 일치하는 요소가 없습니다. selector와 태그 이름을 동일하게 만듭니다.
"로드 중"에서 멈춤, "@for 루프에는 트랙 표현식이 있어야 합니다." track 제외 track item.id 또는 track $index 추가
목록은 늘어나지만 계산된 개수는 잘못된 상태로 유지됩니다. push로 어레이가 변경되었습니다. update()를 사용하고 새 배열을 반환합니다.
Enter를 누르면 페이지가 다시 로드되고 새 항목이 사라집니다. 페이지 자체에 제출된 양식 핸들러에서 event.preventDefault()를 호출하세요.

오류가 전혀 나타나지 않으면 HTML 파일에서 JavaScript가 작동하지 않는 이유는 모든 페이지에 적용되는 검사를 거칩니다.

링크로 공유하세요

Angular 페이지는 설명하는 것보다 보여주는 것이 더 쉽습니다. 스크린샷을 클릭할 수 없으며 .html 첨부 파일이 일반 코드로 열리거나 전화기에서 전혀 열리지 않을 수 있습니다.

작업 버전을 보내려면 페이지를 NOS 문서에 붙여넣고 공유 링크 만들기를 선택하세요. 링크할 HTML이 이를 안내합니다.

페이지는 작성된 대로 렌더링되고 스크립트가 실행되며 Angular 가져오기는 jsDelivr에서 로드되므로 페이지를 받는 사람들은 계정 없이도 앱을 직접 사용할 수 있습니다. 나중에 코드를 변경하면 동일한 링크에 새 버전이 표시됩니다.

자주 묻는 질문

Node.js, npm 또는 Angular CLI 없이 Angular를 실행할 수 있나요?

예, 작은 페이지용입니다. 모듈 스크립트의 CDN에서 @angular/compiler, @angular/core 및 @angular/platform-browser를 가져오면 페이지가 열릴 때 브라우저가 템플릿을 컴파일합니다. 이 페이지의 예제는 그런 식으로 작동합니다. 더 큰 앱은 Angular CLI를 사용하여 구축됩니다.

내 Angular 페이지가 "로드 중..." 상태로 유지되는 이유는 무엇입니까?

Angular가 시작되지 않았으므로 루트 태그 내부의 자리 표시자 텍스트가 여전히 남아 있습니다. 브라우저 콘솔을 엽니다. 일반적인 원인은 누락된 컴파일러 가져오기, type="module"이 없는 스크립트 태그, 태그와 일치하지 않는 선택기 또는 트랙 표현식이 없는 @for 루프입니다.

zone.js가 필요합니까?

현재 Angular에서는 그렇지 않습니다. Angular 문서에서는 영역 없는 변경 감지가 v21의 기본값이며 여기 예제에서는 zone.js를 전혀 로드하지 않는다고 나와 있습니다. 템플릿의 신호 및 이벤트 바인딩은 Angular에 페이지 업데이트 시기를 알려줍니다.

ng-app을 사용하는 AngularJS와 동일합니까?

아니요. AngularJS는 ng-app과 단일 angle.js 스크립트를 사용하는 오래된 프레임워크입니다. 해당 사이트에는 2022년 1월에 지원이 종료된다고 나와 있습니다. 이 페이지의 코드는angular.dev에 문서화된 프레임워크인 Angular용입니다.

일반 HTML 파일에 @Component를 작성할 수 있나요?

여기의 예에서는 이를 피합니다. TypeScript 단계가 없는 일반 JavaScript이므로 동일한 옵션으로 Component()를 호출하고 이를 클래스에 전달합니다. TypeScript의 CLI 프로젝트는 대신 클래스 위에 @Component를 씁니다.