팁: 이 가이드 문서는 핵심 가이드 이후 내용을 다룹니다. 아직 Angular에 익숙하지 않다면 해당 문서를 먼저 읽어보세요.
컴포넌트에서 쿼리 함수(queries) 를 사용하면 자식 엘리먼트를 찾아서 값을 참조할 수 있습니다.
보통은 자식 컴포넌트나 자식 디렉티브, DOM 엘리먼트 등을 참조하는 용도로 사용합니다.
쿼리 함수는 가장 마지막에 찾은 결과를 시그널 타입으로 반환합니다.
그래서 시그널 함수를 실행하면 자식 객체를 참조할 수 있으며, computed
나 effect
함수와 함께 사용해서 반응형 시그널로 활용할 수도 있습니다.
쿼리 함수는 뷰 쿼리(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
에는 일반적으로 ElementRef
나 TemplateRef
를 사용합니다.
컨텐츠의 자식 객체
기본적으로 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
프로퍼티를 구독하면 됩니다.
데코레이터 기반 쿼리 옵션
쿼리 데코레이터는 두번째 인자로 옵션 객체를 받을 수 있습니다. 이 옵션 객체는 시그널 기반 쿼리 함수와 동일하게 동작합니다.
정적 쿼리
@ViewChild
와 ContentChild
데코레이터에 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 에러를 유발할 수 있습니다.