팁: 이 가이드 문서는 핵심 가이드 이후 내용을 다룹니다. 아직 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 {}
만약 쿼리 결과가 없으면 시그널은 undefined 값을 갖습니다.
이 경우는 찾으려는 엘리먼트가 @if로 화면에 표시되지 않은 경우에 발생할 수 있습니다.
그리고 애플리케이션의 상태가 변경되면 contentChild 결과는 계속 최신 상태로 갱신됩니다.
기본적으로 contentChild 쿼리는 하위 자식까지 탐색하지만, contentChildren 쿼리는 바로 한 단계 자식만 찾습니다. 하위 자식 탐색을 참고하세요.
자식 컴포넌트를 여러 개 참조하려면 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 값을 갖습니다.
이 경우는 찾으려는 엘리먼트가 @if나 @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를 사용합니다.
read에 Injector를 전달할 수도 있습니다.
@Component({
selector: 'custom-table',
template: `
<third-party-table #inner>
<ng-template>
<ng-container [ngTemplateOutlet]="columns()" [ngTemplateOutletInjector]="innerInjector()" />
</ng-template>
</third-party-table>
`,
})
export class CustomTable {
columns = contentChild(TemplateRef);
innerInjector = viewChild('inner', {read: Injector});
}
위 예제는 third-party-table 엘리먼트의 노드 인젝터, 즉 트리에서 해당 엘리먼트 위치에 해당하는 인젝터를 가져옵니다.
이 인젝터를 ngTemplateOutletInjector로 NgTemplateOutlet에 전달하면 프로젝션된 템플릿의 디렉티브에서 서드파티 컴포넌트가 제공하는 값을 주입받을 수 있습니다.
컨텐츠의 자식 객체
기본적으로 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 implements AfterViewInit {
@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 implements AfterViewInit {
@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 implements AfterContentInit {
@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 implements AfterContentInit {
@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 implements OnInit {
@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 오류를 유발할 수 있습니다.