포스트

[Angular 마스터하기] Day 14 - 컴포넌트 생명주기, 적재적소 코드 실행

[Angular 마스터하기] Day 14 - 컴포넌트 생명주기, 적재적소 코드 실행

이제와서 시작하는 Angular 마스터하기 - Day 14 “컴포넌트의 생명주기를 이해하고 활용하세요! 🔄”

오늘 배울 내용

  • 생명주기 훅이란
  • 주요 생명주기 훅
  • 최신 방식: afterNextRender, afterRender
  • 실전 활용 예제

1. 생명주기 훅

Angular 컴포넌트는 생성되고, 입력값을 받고, 화면에 렌더링되고, 마지막에는 제거됩니다. 생명주기 훅은 이 과정의 특정 시점에 코드를 실행할 수 있게 해주는 약속입니다.

중요한 기준은 “언제 실행되는가”입니다. API 호출, DOM 접근, 구독 해제처럼 실행 시점이 중요한 코드는 아무 곳에나 넣으면 버그가 생깁니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { Component, OnInit, OnDestroy } from '@angular/core';

@Component({
  selector: 'app-lifecycle-demo',
  template: `<p>생명주기 데모</p>`
})
export class LifecycleDemoComponent implements OnInit, OnDestroy {
  ngOnInit() {
    console.log('컴포넌트 초기화됨');
    // API 호출, 초기 설정 등
  }

  ngOnDestroy() {
    console.log('컴포넌트 제거됨');
    // 정리 작업 (타이머 해제, 구독 해제 등)
  }
}

보통은 ngOnInit에 초기 데이터 로딩을 넣고, ngOnDestroy에 타이머나 구독 정리를 넣습니다. 생성자에는 의존성 주입과 아주 가벼운 초기화만 두는 편이 좋습니다.


2. 주요 생명주기 훅

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@Component({
  template: `<p>{{ message }}</p>`
})

export class LifecycleComponent implements OnInit, OnChanges, OnDestroy {
  @Input() data = '';
  message = '';

  // 1. Input 값이 변경될 때
  ngOnChanges(changes: SimpleChanges) {
    console.log('변경됨:', changes);
  }

  // 2. 컴포넌트 초기화
  ngOnInit() {
    console.log('초기화');
    this.message = '안녕하세요!';
  }

  // 3. 뷰가 초기화된 후
  ngAfterViewInit() {
    console.log('뷰 초기화 완료');
  }

  // 4. 컴포넌트 제거 전
  ngOnDestroy() {
    console.log('정리 작업');
  }
}

자주 쓰는 훅을 표로 보면 더 쉽게 구분할 수 있습니다.

실행 시점 대표 용도
ngOnChanges @Input 값이 바뀔 때 부모가 넘긴 값 변화 감지
ngOnInit 첫 입력값 설정 후 한 번 초기 API 호출, 폼 초기화
ngAfterViewInit 자식 뷰 초기화 후 ViewChild 기반 DOM/컴포넌트 접근
ngOnDestroy 컴포넌트 제거 직전 타이머, 이벤트, 구독 정리

ngOnChanges는 입력값이 있는 컴포넌트에서 특히 유용합니다. 반대로 입력값이 없고 단순히 처음 한 번만 준비하면 된다면 ngOnInit만으로 충분합니다.


3. 최신 방식

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import { Component, afterNextRender, afterRender } from '@angular/core';

@Component({
  selector: 'app-modern-lifecycle',
  template: `<div #content>콘텐츠</div>`
})
export class ModernLifecycleComponent {
  constructor() {
    // 다음 렌더링 후 한 번만 실행
    afterNextRender(() => {
      console.log('렌더링 완료');
    });

    // 매 렌더링마다 실행
    afterRender(() => {
      console.log('렌더링됨');
    });
  }
}

afterNextRender는 다음 렌더링이 끝난 뒤 한 번 실행되고, afterRender는 렌더링이 발생할 때마다 실행됩니다. 화면 크기 측정, 외부 UI 라이브러리 초기화처럼 DOM이 실제로 그려진 뒤 처리해야 하는 작업에 어울립니다.

단, 매 렌더링마다 실행되는 코드는 비용이 커질 수 있습니다. afterRender 안에서 무거운 계산이나 불필요한 상태 변경을 반복하면 성능 문제가 생길 수 있으니, 정말 매번 필요한 작업인지 먼저 확인하세요.


4. 실전 예제

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
@Component({
  selector: 'app-timer',
  template: `
    <div class="timer">
      <h2>{{ seconds }}초</h2>
      <button (click)="toggle()">{{ isRunning ? '정지' : '시작' }}</button>
      <button (click)="reset()">리셋</button>
    </div>
  `
})

export class TimerComponent implements OnInit, OnDestroy {
  seconds = 0;
  isRunning = false;
  private intervalId?: number;

  ngOnInit() {
    console.log('타이머 컴포넌트 시작');
  }

  toggle() {
    this.isRunning = !this.isRunning;

    if (this.isRunning) {
      this.intervalId = window.setInterval(() => {
        this.seconds++;
      }, 1000);
    } else {
      this.clearTimer();
    }
  }

  reset() {
    this.seconds = 0;
    this.isRunning = false;
    this.clearTimer();
  }

  private clearTimer() {
    if (this.intervalId) {
      clearInterval(this.intervalId);
      this.intervalId = undefined;
    }
  }

  ngOnDestroy() {
    console.log('타이머 정리');
    this.clearTimer();  // 메모리 누수 방지!
  }
}

이 예제에서 가장 중요한 부분은 ngOnDestroy입니다. 컴포넌트가 사라졌는데도 setInterval이 계속 살아 있으면 화면에는 보이지 않는 코드가 계속 실행됩니다. 이런 문제가 쌓이면 메모리 누수나 예상치 못한 상태 변경으로 이어집니다.

Angular의 최신 코드에서는 DestroyRef를 활용해 정리 로직을 더 가까운 위치에 둘 수도 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import { Component, DestroyRef, inject } from '@angular/core';

@Component({
  selector: 'app-cleanup-demo',
  template: `<p>정리 예제</p>`
})
export class CleanupDemoComponent {
  private destroyRef = inject(DestroyRef);

  constructor() {
    const id = window.setInterval(() => {
      console.log('작업 중');
    }, 1000);

    this.destroyRef.onDestroy(() => {
      clearInterval(id);
    });
  }
}

ngOnDestroyDestroyRef 중 무엇을 쓰든 핵심은 같습니다. 컴포넌트가 만든 자원은 컴포넌트가 사라질 때 함께 정리해야 합니다.


📝 정리

생명주기 순서

  1. ngOnChanges - Input 변경
  2. ngOnInit - 초기화
  3. ngAfterViewInit - 뷰 초기화
  4. ngOnDestroy - 정리

체크리스트

  • ngOnInit을 사용할 수 있나요?
  • ngOnDestroy로 정리 작업을 할 수 있나요?
  • 타이머를 만들어봤나요?
  • @Input 변경에는 ngOnChanges가 필요한지 판단할 수 있나요?
  • DOM 렌더링 이후 작업에 afterNextRender를 고려할 수 있나요?

📚 다음 학습


“생명주기를 이해하면 더 나은 컴포넌트를 만들 수 있습니다!” 🔄

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.