심화 가이드
컴포넌트

쿼리 함수로 자식 컴포넌트 참조하기

팁: 이 가이드 문서는 핵심 가이드 이후 내용을 다룹니다. 아직 Angular에 익숙하지 않다면 해당 문서를 먼저 읽어보세요.

컴포넌트에서 쿼리 함수(queries) 를 사용하면 자식 엘리먼트를 찾아서 값을 참조할 수 있습니다.

보통은 자식 컴포넌트나 자식 디렉티브, DOM 엘리먼트 등을 참조하는 용도로 사용합니다.

쿼리 함수는 가장 마지막에 찾은 결과를 시그널 타입으로 반환합니다. 그래서 시그널 함수를 실행하면 자식 객체를 참조할 수 있으며, computedeffect 함수와 함께 사용해서 반응형 시그널로 활용할 수도 있습니다.

쿼리 함수는 뷰 쿼리(view queries)컨텐츠 쿼리(content queries) , 이렇게 두 종류가 있습니다.

뷰 쿼리(View queries)

뷰 쿼리는 컴포넌트 , 즉, 컴포넌트 템플릿 자체에 있는 엘리먼트를 찾습니다. 뷰 쿼리 함수 중 viewChild 함수를 사용하면 원하는 엘리먼트를 하나 참조할 수 있습니다.

      
@Component({  selector: 'custom-card-header',  /*...*/})export class CustomCardHeader {  text: string;}@Component({  selector: 'custom-card',  template: '<custom-card-header>Visit sunny California!</custom-card-header>',})export class CustomCard {  header = viewChild(CustomCardHeader);  headerText = computed(() => this.header()?.text);}

이 예제에서 CustomCard 컴포넌트는 자식 CustomCardHeader를 찾아 오고, 이 결과를 computed로 다시 한 번 참조합니다.

만약, 쿼리 결과가 없으면 시그널은 undefined 값을 갖습니다. 이 경우는 찾으려는 엘리먼트가 @if 로 화면에 표시되지 않은 경우에 발생할 수 있습니다. 그리고 애플리케이션의 상태가 변경되면 viewChild 결과는 계속 최신 상태로 갱신됩니다.

자식 컴포넌트를 여러개 참조하려면 viewChildren 함수를 사용하면 됩니다.

      
@Component({  selector: 'custom-card-action',  /*...*/})export class CustomCardAction {  text: string;}@Component({  selector: 'custom-card',  template: `    <custom-card-action>Save</custom-card-action>    <custom-card-action>Cancel</custom-card-action>  `,})export class CustomCard {  actions = viewChildren(CustomCardAction);  actionsTexts = computed(() => this.actions().map(action => action.text);}

viewChildren 함수는 탐색 결과를 Array 타입으로 반환하는 시그널입니다.

쿼리 함수는 컴포넌트 밖을 참조할 수 없습니다. 뷰 쿼리는 컴포넌트 템플릿 안에 있는 객체만 참조할 수 있습니다.

컨텐츠 쿼리(Content queries)

컨텐츠 쿼리는 컴포넌트 컨텐츠, 즉, 컴포넌트 템플릿 안쪽에 있는 엘리먼트를 찾습니다. 컨텐츠 쿼리 함수 중 contentChild 함수를 사용하면 원하는 엘리먼트를 하나 참조할 수 있습니다.

      
@Component({  selector: 'custom-toggle',  /*...*/})export class CustomToggle {  text: string;}@Component({  selector: 'custom-expando',  /*...*/})export class CustomExpando {  toggle = contentChild(CustomToggle);  toggleText = computed(() => this.toggle()?.text);}@Component({   /* ... */  // CustomToggle은 CustomExpando 안에 컨텐츠로 사용되었습니다.    template: `    <custom-expando>      <custom-toggle>Show</custom-toggle>    </custom-expando>  `})export class UserProfile { }

위 예제에서 CustomExpando 컴포넌트는 자식 CustomToggle 컴포넌트를 찾아 오고, 이 결과를 computed로 다시 한 번 참조합니다.

만약 쿼리 결과가 없으면 시그널은 undefined 값을 갖습니다. 이 경우는 찾으려는 엘리먼트가 @if로 화면에 표시되지 않은 경우에 발생할 수 있습니다. 그리고 애플리케이션의 상태가 변경되면 contentChild 결과는 계속 최신 상태로 갱신됩니다.

기본적으로 컨텐츠 쿼리는 컴포넌트의 한 단계 아래 자식들을 찾으며, 그보다 자식 컴포넌트는 탐색하지 않습니다.

자식 컴포넌트를 여러개 참조하려면 contentChildren 함수를 사용하면 됩니다.

      
@Component({  selector: 'custom-menu-item',  /*...*/})export class CustomMenuItem {  text: string;}@Component({  selector: 'custom-menu',  /*...*/})export class CustomMenu {  items = contentChildren(CustomMenuItem);  itemTexts = computed(() => this.items().map(item => item.text));}@Component({  selector: 'user-profile',  template: `    <custom-menu>      <custom-menu-item>Cheese</custom-menu-item>      <custom-menu-item>Tomato</custom-menu-item>    </custom-menu>  `})export class UserProfile { }

contentChildren 함수는 탐색 결과를 Array 타입으로 반환하는 시그널입니다.

쿼리 함수는 컴포넌트 밖을 참조할 수 없습니다. 컨텐츠 쿼리는 컴포넌트 템플릿 안에 있는 객체만 참조할 수 있습니다.

필수 쿼리(Required queries)

viewChild 함수나 contentChild 함수를 사용해서 자식 컴포넌트를 탐색했지만 아무것도 찾지 못한 경우, 쿼리 함수가 반환하는 시그널은 undefined 값을 갖습니다. 이 경우는 찾으려는 엘리먼트가 @fi@for로 인해 화면에 표시되지 않는 경우에 발생할 수 있습니다. 결국 자식 엘리먼트를 쿼리하는 함수는 찾으려는 객체 타입이거나 undefined 타입일 수 있습니다.

하지만 자식 컴포넌트가 반드시 존재하는 경우를 간주할 수 있습니다. 이 경우에는 대상 컴포넌트가 반드시 존재한다는 것을 지정해서 값이 반드시 존재하는 쿼리로 처리할 수 있습니다.

      
@Component({/* ... */})export class CustomCard {  header = viewChild.required(CustomCardHeader);  body = contentChild.required(CustomCardBody);}

필수 쿼리로 지정했지만 대상을 찾지 못하는 경우에는 에러가 발생합니다. 왜냐하면 필수 쿼리는 대상이 있는 것을 보장하기 때문에 시그널이 undefined 값을 가질 수 없기 때문입니다.

쿼리 구분자(Query locators)

쿼리 데코레이터의 첫번째 인자는 구분자(locator) 입니다.

대부분의 경우, 구분자는 컴포넌트나 디렉티브를 그대로 사용합니다.

아니면 템플릿 참조 변수를 활용해서 문자열 구분자를 사용할 수도 있습니다.

      
@Component({  /*...*/  template: `    <button #save>Save</button>    <button #cancel>Cancel</button>  `})export class ActionBar {  saveButton = viewChild<ElementRef<HTMLButtonElement>>('save');}

템플릿에 구분자가 여러개 매칭되는 경우에는 첫번째 매칭되는 엘리먼트를 참조합니다.

CSS 셀렉터는 쿼리 구분자로 사용할 수 없습니다.

쿼리 함수와 인젝터 계층

팁: 프로바이더와 인젝션 트리 계층에 대해 알아보려면 의존성 주입(Dependency Injection) 문서를 참고하세요.

ProviderToken을 구분자로 사용하는 고급 활용 방식도 있습니다. 이 방식을 활용하면 컴포넌트 프로바이더나 디렉티브 프로바이더로 자식 객체를 탐색할 수 있습니다.

      
const SUB_ITEM = new InjectionToken<string>('sub-item');@Component({  /*...*/  providers: [{provide: SUB_ITEM, useValue: 'special-item'}],})export class SpecialItem { }@Component({/*...*/})export class CustomList {  subItemType = contentChild(SUB_ITEM);}

위 예제에서는 InjectionToken을 구분자로 사용했지만, ProviderToken 타입 중 어떠한 것을 사용해도 됩니다.

쿼리 옵션

쿼리 함수는 두번째 인자로 옵션 객체를 받을 수 있습니다. 옵션을 지정하면 쿼리 함수가 어떻게 객체를 찾을지 지정합니다.

엘리먼트 인젝터로 원하는 값 읽기

기본적으로 쿼리 구분자는 엘리먼트 자체와 엘리먼트의 값을 모두 의미합니다. read 옵션을 사용하면 구분자와 매칭되는 객체를 직접 지정할 수 있습니다.

      
@Component({/*...*/})export class CustomExpando {  toggle = contentChild(ExpandoContent, {read: TemplateRef});}

위 예제 코드처럼 작성하면 Angular는 ExpandoContent를 찾아서 해당 엘리먼트의 TemplateRef를 반환합니다.

read에는 일반적으로 ElementRefTemplateRef를 사용합니다.

컨텐츠의 자식 객체

기본적으로 contentChildren 쿼리 함수는 컴포넌트의 바로 한 단계 자식 컴포넌트를 탐색하며 더 하위 자식은 탐색하지 않습니다. 반면에 contentChild 쿼리 함수는 자식 엘리먼트를 탐색합니다.

      
@Component({  selector: 'custom-expando',  /*...*/})export class CustomExpando {  toggle = contentChildren(CustomToggle); // 찾을 수 없음  // toggle = contentChild(CustomToggle); // 찾음}@Component({  selector: 'user-profile',  template: `    <custom-expando>      <some-other-component>        <custom-toggle>Show</custom-toggle>      </some-other-component>    </custom-expando>  `})export class UserProfile { }

위 예제 코드에서 <custom-toggle> 컴포넌트는 CustomExpando의 직접적인 자식 컴포넌트가 아니기 때문에 <custom-toggle>를 탐색할 수 없습니다. 이 경우 descendants: true 옵션을 지정하면 쿼리 함수가 자식 컴포넌트를 모두 탐색합니다. 하지만 이런 경우에도 컴포넌트를 벗어나는 영역은 절대 탐색할 수 없습니다.

뷰 쿼리는 항상 자식 컴포넌트를 참조하기 땜누에 이 옵션을 사용하지 않습니다.

데코레이터 기반 쿼리 함수

팁: 기존에 사용하던 데코레이터 기반 쿼리 API도 온전히 잘 작동하지만, 시그널 기반 쿼리 함수 사용을 권장합니다.

쿼리 함수는 데코레이터 프로퍼티로 사용할 수도 있습니다. 데코레이터 기반 쿼리 함수는 시그널 기반 쿼리 함수와 동일하게 동작합니다.

뷰 쿼리

대상을 하나만 참조하려면 @ViewChild 데코레이터를 사용합니다.

      
@Component({  selector: 'custom-card-header',  /*...*/})export class CustomCardHeader {  text: string;}@Component({  selector: 'custom-card',  template: '<custom-card-header>Visit sunny California!</custom-card-header>',})export class CustomCard {  @ViewChild(CustomCardHeader) header: CustomCardHeader;  ngAfterViewInit() {    console.log(this.header.text);  }}

위 코드는 CustomCard 컴포넌트가 자식 컴포넌트 CustomCardHeader를 탐색하며, 탐색 결과는 ngAfterViewInit에서 접근하는 코드입니다.

그리고 애플리케이션 상태가 변경되면 @ViewChild 탐색 결과는 최신 상태로 갱신됩니다.

뷰 쿼리는 ngAfterViewInit 라이프싸이클 메서드 시점부터 결과값을 참조할 수 있습니다. 이 메서드가 실행되기 전 시점에는 undefined 값을 갖습니다. 컴포넌트 라이프싸이클에 대해 알아보려면 라이프싸이클 후킹 함수 문서를 참고하세요.

자식 컴포넌트를 여러개 탐색하려면 @ViewChildren 데코레이터를 사용하면 됩니다.

      
@Component({  selector: 'custom-card-action',  /*...*/})export class CustomCardAction {  text: string;}@Component({  selector: 'custom-card',  template: `    <custom-card-action>Save</custom-card-action>    <custom-card-action>Cancel</custom-card-action>  `,})export class CustomCard {  @ViewChildren(CustomCardAction) actions: QueryList<CustomCardAction>;  ngAfterViewInit() {    this.actions.forEach(action => {      console.log(action.text);    });  }}

@ViewChildren 을 사용하면 QueryList 타입을 반환합니다. 그리고 애플리케이션 상태가 변경되는 것에 따라 쿼리 결과가 달라지는 것을 확인하려면 changes 프로퍼티를 구독하면 됩니다.

컨텐츠 쿼리

대상을 하나만 참조하려면 @ContentChild 데코레이터를 사용합니다.

      
@Component({  selector: 'custom-toggle',  /*...*/})export class CustomToggle {  text: string;}@Component({  selector: 'custom-expando',  /*...*/})export class CustomExpando {  @ContentChild(CustomToggle) toggle: CustomToggle;  ngAfterContentInit() {    console.log(this.toggle.text);  }}@Component({  selector: 'user-profile',  template: `    <custom-expando>      <custom-toggle>Show</custom-toggle>    </custom-expando>  `})export class UserProfile { }

위 코드는 CustomExpando 컴포넌트가 자식 컴포넌트 CustomToggle을 탐색하며, 탐색 결과는 ngAfterContentInit에서 접근하는 코드입니다.

그리고 애플리케이션 상태가 변경되면 @ContentChild 탐색 결과는 최신 상태로 갱신됩니다.

컨텐츠 쿼리는 ngAfterContentInit 라이프싸이클 메서드 시점부터 결과값을 참조할 수 있습니다. 이 메서드가 실행되기 전 시점에는 undefined 값을 갖습니다. 컴포넌트 라이프싸이클에 대해 알아보려면 라이프싸이클 후킹 함수 문서를 참고하세요.

자식 컴포넌트를 여러개 탐색하려면 @ContentChildren 데코레이터를 사용하면 됩니다.

      
@Component({  selector: 'custom-menu-item',  /*...*/})export class CustomMenuItem {  text: string;}@Component({  selector: 'custom-menu',  /*...*/})export class CustomMenu {  @ContentChildren(CustomMenuItem) items: QueryList<CustomMenuItem>;  ngAfterContentInit() {    this.items.forEach(item => {      console.log(item.text);    });  }}@Component({  selector: 'user-profile',  template: `    <custom-menu>      <custom-menu-item>Cheese</custom-menu-item>      <custom-menu-item>Tomato</custom-menu-item>    </custom-menu>  `})export class UserProfile { }

@ContentChildren 을 사용하면 QueryList 타입을 반환합니다. 그리고 애플리케이션 상태가 변경되는 것에 따라 쿼리 결과가 달라지는 것을 확인하려면 changes 프로퍼티를 구독하면 됩니다.

데코레이터 기반 쿼리 옵션

쿼리 데코레이터는 두번째 인자로 옵션 객체를 받을 수 있습니다. 이 옵션 객체는 시그널 기반 쿼리 함수와 동일하게 동작합니다.

정적 쿼리

@ViewChildContentChild 데코레이터에 static 옵션을 지정할 수 있습니다.

      
@Component({  selector: 'custom-card',  template: '<custom-card-header>Visit sunny California!</custom-card-header>',})export class CustomCard {  @ViewChild(CustomCardHeader, {static: true}) header: CustomCardHeader;  ngOnInit() {    console.log(this.header.text);  }}

static: true 옵션을 지정하면 찾으려는 대상 컴포넌트가 언제나 존재하며 렌더링 조건에 영향을 받지 않는다는 것을 의미합니다. 그래서 탐색 결과를 빠른 시점부터 참조할 수 있으며, ngOnInit 라이프싸이클 메서드에서 활용할 수도 있씁니다.

정적 쿼리 결과는 처음 초기화된 후에 변경되지 않습니다.

static 옵션은 @ViewChildren 쿼리 함수나 @ContentChildren 쿼리 함수에는 사용할 수 없습니다.

QueryList 활용하기

@ViewChildren@ContentChildren은 모두 탐색 결과를 QueryList 타입으로 반환합니다.

QueryList는 배열과 비슷하게 map이나 reduce, forEach와 같은 API를 제공하며, toArray 메서드를 활용하면 일반적인 배열 형태로 결과물을 참조할 수 있습니다.

그리고 changes 프로퍼티를 구독하면 탐색 결과가 변경되는 것을 추적할 수도 있습니다.

쿼리를 잘못 사용하는 경우

쿼리를 사용할 때 혼동하는 경우에는 코드를 복잡하고 유지보수하기 어렵게 만들 수 있습니다.

여러 컴포넌트에 사용되는 소스가 있다면 항상 단일한 소스로 유지하세요. 컴포넌트끼리 동기화되지 않아 발생하는 문제를 예방하는 방법입니다.

자식 컴포넌트의 상태값을 직접 설정하지 마세요. 이런 방식은 이해하기 어렵고 ExpressionChangedAfterItHasBeenChecked 에러를 유발할 수 있습니다.

부모 컴포넌트나 더 위쪽 컴포넌트의 상태값을 직접 설정하지 마세요. 이런 방식은 이해하기 어렵고 ExpressionChangedAfterItHasBeenChecked 에러를 유발할 수 있습니다.