Engineering
대규모 앵귤러 웹 애플리케이션 성능 최적화: 카카오 챗봇 관리자센터 사례
gling.9카카오
2025년 1월 23일
원문에서 보기 ↗안녕하세요, 카카오 FE 플랫폼 조직의 톡메시지 FE에서 챗봇 관리자센터를 개발하고 있는 글링입니다.
이번 글에서는 챗봇 관리자센터의 성능 최적화 경험을 공유해 보려고 합니다. 챗봇 관리자센터는 앵귤러(Angular), RxJS, 타입스크립트(TypeScript)를 이용해 개발하고 있습니다. 따라서 보편적인 웹 애플리케이션의 성능과 관련된 내용부터 앵귤러 애플리케이션의 특성을 활용한 성능 개선 방법까지, 실제 사례를 중심으로 다뤄보겠습니다.
개요
챗봇 관리자센터 소개
챗봇 관리자센터는 카카오 인공지능 기술을 기반으로 한 AI 설계 플랫폼으로서, 기업 및 기관에서 카카오톡 채널을 통한 AI 챗봇 서비스를 손쉽게 구축하고 운영할 수 있도록 지원합니다. 쉽게 말해서 평소 여러분이 다양한 채널로부터 수신하고 있는 메시지를 발송할 수 있는 봇을 만드는 플랫폼이라고 할 수 있습니다. (아래 출처: 카카오 챗봇 관리자센터 도움말)



프로젝트 배경
카카오 FE 플랫폼 조직은 사용자가 경험하는 서비스 품질을 핵심 가치로 설정하고, 이를 향상시키기 위해 다양한 시도를 하고 있습니다. 서비스 품질 유지를 위한 공통 목표 중 하나는 ‘성능 개선’으로 더 나은 성능을 제공하기 위해 지속적으로 노력하고 있습니다. 챗봇 관리자센터는 화면 전환 시 발생하는 레이아웃 변경이나 느린 로딩 속도를 개선하여, 사용자가 체감할 수 있는 수준의 성능 개선을 목표로 프로젝트를 진행했습니다.

목표 및 개선 방향
카카오 FE 플랫폼 조직은 표준 웹 페이지의 성능 측정 도구인 구글의 Lighthouse를 기반으로, 자체 개발한 성능 측정 도구인 파루스(Pharus)를 사용하고 있습니다. 파루스는 6시간 간격으로 등록된 페이지들의 성능을 측정하고 리포트를 생성해 주기 때문에, 웹 페이지의 성능을 효율적으로 관리할 수 있습니다. 또한 HTTP 헤더, 측정 환경, 쿠키, 로컬 스토리지 등 웹 페이지 성능 측정을 위한 환경도 손쉽게 구성할 수 있습니다. 본 글에서 제시하는 모든 성능 점수는 파루스를 통해 측정된 값입니다.
챗봇 관리자센터는 LNB에서 접근할 수 있는 주요 페이지들의 성능 점수를 85점 이상으로 향상시키는 것을 정량적인 목표로 설정했습니다. 성능 분석 결과, 대다수의 페이지에서 Google Lighthouse 성능 지표 중 CLS(Cumulative Layout Shift)와 TBT(Total Blocking Time) 지표가 높게 나타나 성능 저하의 주요 원인으로 확인되었습니다. 이에 따라, CLS 및 TBT 지표 최적화를 성능 개선의 핵심 방향으로 설정했습니다.
성능 개선 내용
CLS 개선
CLS란?
CLS(Cumulative Layout Shift)는 웹 페이지 로딩 과정에서 예상치 못한 누적 레이아웃 이동을 의미하는 지표입니다. 이는 크기가 지정되지 않은 이미지, 동적으로 삽입되는 요소 등 기술적 요인으로 인해 발생하는 레이아웃 시프트(Layout Shift)를 측정합니다. CLS는 사용성에 직접적인 영향을 미치기 때문에, 웹 페이지의 전반적인 성능과 사용성을 최적화하기 위해서는 CLS가 최소화되어야 합니다.

데이터 로드 시점의 레이아웃 시프트 개선
챗봇 관리자센터의 일부 페이지에서는 진입 시 기본 화면이 렌더링 되고 데이터 로드 완료 시점에 화면이 갱신되면서 레이아웃 이동이 발생하고 있었습니다. 이러한 레이아웃 이동을 방지하기 위한 접근 방식으로 데이터 로드 중 로딩 화면을 보여주거나, 임의로 특정 컴포넌트의 렌더링을 지연하는 방법 등이 있습니다. 전자는 새로운 로딩 화면 디자인이 필요하고, 후자는 데이터 로드 여부를 나타내는 플래그 변수와 라이프 사이클 훅을 통해 렌더링 시점을 제어해야 합니다. 하지만 플래그 변수로 렌더링을 제어하는 방식은 코드의 가독성을 떨어뜨리고, 상태 관리가 복잡해질 수 있기 때문에 지양하는 것이 좋습니다.


그렇다면 데이터가 준비된 후 화면을 전환하는 건 어떨까요? 앵귤러에서 제공하는 ResolverFn은 라우팅 과정에서 데이터를 미리 로드하는 기능입니다. ResolverFn을 통해 라우터가 특정 경로를 활성화하기 전에 설정한 함수에서 데이터가 완전히 로드될 때까지 대기할 수 있습니다. 이렇게 하면 데이터 로드와 화면 렌더링 시점이 일치하게 되므로, 불필요한 레이아웃 이동을 방지하고 사용자 경험을 개선할 수 있습니다.


ResolverFn 적용 후 Performance Insight 탭에서도 레이아웃 시프트가 개선된 것을 확인할 수 있습니다.

ResolverFn 사용 시에는 아래의 내용들을 주의해야 합니다.
첫 번째로 예외 처리의 필요성입니다. ResolverFn에서 에러가 발생할 경우, 라우팅이 중단되어 페이지에 진입할 수 없습니다. 이러한 상황을 방지하기 위해 ResolverFn에서 에러 발생 시, 에러 페이지로 리다이렉트 하는 등의 적절한 예외 처리가 필요합니다.
두 번째는 페이지 진입 지연입니다. ResolverFn 특성상 데이터 로드가 완료된 후에 라우팅 되기 때문에, 복잡한 데이터 처리나 대용량 데이터 로드 시 페이지 진입이 지연될 수 있습니다. 따라서 불필요한 API 호출이나 과도한 데이터 전처리 작업에는 ResolverFn 사용을 지양해야 합니다.
공통 컴포넌트의 레이아웃 시프트 개선
챗봇 관리자센터 주요 페이지들의 성능 분석 결과, 각 페이지 컴포넌트의 wrapper 요소에서 공통적으로 레이아웃 시프트가 발생하고 있었습니다.

왜 이런 현상이 발생하고 있었을까요? 챗봇 관리자센터에서는 다양한 페이지들의 스타일을 제어하기 위한 kakao-content-style라는 공통 컴포넌트가 있습니다. kakao-content-style 컴포넌트는 kakaoContent라는 id를 가진 DOM 요소를 찾고 동적으로 class를 삽입해 페이지 별로 필요한 스타일을 적용하는 역할을 합니다.


// kakao-content-style.ts
import {Component, Input} from '@angular/core';
@Component({
selector: 'kakao-content-style',
templateUrl: './kakao-content-style.html',
})
export class KakaoContentStyleComponent {
@Input()
class: string;
constructor() {
const kakaoContent \= document.getElementById('kakaoContent');
if (kakaoContent) {
kakaoContent.className \= this.class || '';
}
setTimeout(() \=\> {
const newContent \= document.getElementById('kakaoContent');
if (newContent?.className \=== '' && this.class \!== '') {
newContent.className \= this.class;
}
});
}
}
kakao-content-style 컴포넌트는 다음과 같은 문제가 있습니다.
1. constructor 내에서 DOM 접근
constructor에서 DOM을 직접 조작하는 방식은 앵귤러의 컴포넌트 라이프 사이클에 적합하지 않습니다. constructor는 컴포넌트가 생성되는 시점에 호출되지만, 이 시점에서는 아직 앵귤러의 변경 감지, 데이터 바인딩, 뷰 초기화 등의 작업이 완료되지 않아 DOM의 상태를 보장할 수 없기 때문입니다.
2. 비동기적으로 DOM요소에 클래스 추가
위에서 언급한 대로 constructor에서는 id가 kakaoContent인 요소가 DOM에 존재하는 것을 보장할 수 없기 때문에 setTimeout을 통해 클래스를 추가하는 작업을 중복으로 수행하고 있습니다. 이처럼 클래스가 비동기적으로 추가되면 사용자 화면에서 레이아웃 시프트가 발생할 수 있습니다.
아래는 개선된 kakao-content-style 컴포넌트 코드입니다. 앵귤러 라이프 사이클 훅인 ngAfterViewInit에서 클래스를 추가하도록 변경되었습니다. ngAfterViewInit 훅은 컴포넌트와 자식 컴포넌트의 뷰가 초기화된 후 호출되므로, 이 시점에는 #kakaoContent 요소가 DOM에 존재함을 보장할 수 있습니다. 또한 ngAfterViewInit 훅은 컴포넌트의 라이프 사이클 주기 내에서 한 번만 호출되기 때문에 불필요하게 여러 번 스타일을 적용하지 않습니다.
// kakao-content-style.ts 개선 후
import {Component, Input, Renderer2, AfterViewInit} from '@angular/core';
@Component({
selector: 'kakao-content-style',
templateUrl: './kakao-content-style.html',
})
export class KakaoContentStyleComponent implements AfterViewInit {
@Input()
private contentClass: string;
constructor(private renderer: Renderer2) {}
ngAfterViewInit() {
const contentElement = document.getElementById('kakaoContent');
if (contentElement) {
this.renderer.setAttribute(contentElement, 'class', this.contentClass || '');
}
}
}

개선 후 페이지 컴포넌트의 wrapper 요소에서 발생하던 레이아웃 시프트가 제거되었습니다. 대부분 페이지의 CLS 수치가 0에 가깝게 개선되면서 성능 점수도 약 8~10점씩 향상되었습니다.

TBT 개선
TBT란?
TBT(Total Blocking Time)는 웹 페이지 로드 과정에서 메인 스레드 작업으로 인해 사용자의 입력(예: 클릭, 키 입력 등)이 처리되지 못한 총시간을 나타내는 성능 지표입니다.
구체적으로, TBT는 웹 페이지에서 첫 번째 콘텐츠가 표시된 시점(FCP, First Contentful Paint)부터 사용자가 페이지와 상호작용할 수 있게 되는 시점(TTI, Time to Interactive)까지 측정됩니다. 이 구간에서 메인 스레드의 각 작업(Task)중 실행 시간이 50ms를 초과하는 부분을 합산하여 계산됩니다. 50ms를 초과하는 작업을 롱 태스크(Long Task)라고 하며, 롱 태스크가 많을수록 메인 스레드가 사용자 입력을 처리하지 못하는 시간이 길어져 TBT 수치가 증가하게 됩니다.
주요 롱 태스크 유발 요인
- 복잡한 JavaScript 실행
- 대규모 DOM 업데이트
- 메인 스레드에서 블로킹 작업(예: 렌더링, 스타일 재계산)


TTI는 FCP 이후, 메인스레드에서 롱 태스크가 5초 이상 발생하지 않고, 네트워크 요청(GET)이 2개 이하로 유지되며, 사용자 입력에 빠르게 응답할 수 있는 시점에 결정됩니다. TBT는 FCP와 TTI 사이에서 발생하는 롱 태스크를 측정하는 지표이므로, TTI 시점을 단축하고 롱태스크를 줄이는 것이 TBT 개선에 효과적입니다. 이를 위해 다음과 같은 방법을 고려할 수 있습니다.
- JavaScript 코드 분리 및 최적화
- 비동기 처리(Web Worker 등) 활용
- 중복되거나 불필요한 DOM 업데이트 제거
메인 스레드란?
브라우저의 렌더러 프로세스는 코드를 사용자와 상호작용할 수 있는 웹 페이지로 변환하는 역할을 수행합니다. 렌더러 프로세스의 메인 스레드는 UI 스레드라고도 불리며, 아래와 같은 작업들을 처리합니다.
- HTML 파싱과 DOM 빌드: HTML 코드를 파싱해 DOM을 생성
- CSS 파싱 후 지정된 스타일 적용: CSS를 파싱 한 후, DOM에 지정된 스타일을 계산하고 적용
- Javascript 파싱, 평가 및 실행: JavaScript 코드를 파싱하고 실행해 동적인 동작을 처리
메인 스레드는 이러한 작업과 더불어 사용자 이벤트(예: 클릭, 키보드 입력 등) 처리도 담당합니다. 따라서 메인 스레드가 과도한 부하 상태가 되면, 웹 페이지는 사용자 응답에 반응할 수 없어 사용자 경험에 부정적인 영향을 미칠 수 있습니다. 메인 스레드의 활동은 크롬 개발자 도구의 Performance 탭에서 시각적으로 모니터링할 수 있습니다. 이를 활용해 메인 스레드의 부하 원인을 분석하고 성능 개선을 위한 작업들의 우선순위를 결정할 수 있습니다.
- Main 섹션에는 메인 스레드 활동 로그가 시간 순서대로 왼쪽에서 오른쪽으로 표시됩니다.
- 화면의 위에서 아래로는 이벤트가 발생한 원인(예: 스타일 계산, 스크립트 실행 등)이 나열됩니다.

성능 측정 결과 중 ‘기본 스레드 작업 최소화하기’ 섹션에서는 메인 스레드가 처리하는 다양한 작업들을 확인할 수 있습니다. 스크립트 평가, 스타일 및 레이아웃, HTML 및 CSS 파싱, 렌더링, 가비지 컬렉션 등의 작업이 포함되며, 특히 챗봇 관리자센터에서는 스크립트 평가에 오랜 시간이 소요되는 것으로 나타났습니다.

컴포넌트 레이지 로드 구현
챗봇 관리자센터에는 사용자들의 봇 사용 행태를 시각적으로 확인할 수 있는 통계/인사이트 메뉴를 제공합니다.



통계/인사이트 메뉴의 하위 페이지들 중 표와 차트를 포함한 페이지들은 모두 높은 TBT 수치로 인해 성능 점수가 60~70점대에 머무르고 있었습니다. 특히 ‘통계 > 대시보드’, ‘인사이트 > 사용자’ 페이지의 성능이 현저히 떨어지고 있었는데, 두 페이지 모두 4개의 차트를 렌더링 한다는 공통점을 가지고 있었습니다. Performance 탭을 통해 성능 저하의 원인을 상세히 파악해 보겠습니다.


두 페이지 모두 롱 태스크 구간에서 주로 차트 렌더링을 위한 차트 라이브러리의 메서드(wrappedUpdateOrCreateChart, updateOrCreateChart)가 실행되고 있습니다. 또한, 차트 렌더링 과정에서 챗봇 관리자센터의 커스텀 아이콘 삽입을 위해 데이터를 순회하며 툴팁 옵션 수정과 SVG 이미지 변경 등의 추가 작업이 수행되어 메인 스레드에 부하를 가중시키고 있습니다.
이미 차트 라이브러리의 최적화 옵션이 적용된 상태였기 때문에, 롱 태스크를 줄이기 위한 새로운 접근이 필요했습니다. 페이지 구조를 분석한 결과, 통계/인사이트 페이지가 스크롤 시 순차적으로 차트를 표시하는 구조라는 점을 활용해, 페이지 진입 시 보이는 차트만 우선 렌더링 하고, 나머지 차트는 지연 로드하는 방식을 시도했습니다.
컴포넌트 레이지 로드를 위한 다양한 방법이 있는데, 이번 작업에서는 앵귤러 17 버전부터 제공되는 Deferrable Views를 활용해 보았습니다. 이 기능은 Lazy load 대상 컴포넌트를 별도의 번들 파일로 분리하기 때문에 번들 크기 최적화 및 초기 로딩 속도 향상을 기대할 수 있습니다.
@defer 적용 후 차트 컴포넌트가 별도의 번들로 분리되었습니다. 페이지 상단에서 노출되는 차트 컴포넌트는 브라우저가 유휴 상태(Idle time) 일 때 번들을 로드해 렌더링 하도록 설정하고(default 옵션), 그 외에 스크롤 시 보이는 차트들은 뷰포트에 차트의 타이틀이 포함될 때 렌더링 되도록 설정했습니다.



@defer가 잘 적용되어 의도대로 동작하고 있습니다. 이제 성능 변화를 확인해 보겠습니다.
롱 태스크 구간이 눈에 띄게 줄어들었으며, 차트 렌더링과 관련된 wrappedUpdateOrCreateChart 메서드의 실행 횟수가 347에서 4로 줄어든 것을 확인할 수 있습니다.


인사이트 > 사용자 페이지 역시 롱 태스크 구간이 줄고 wrappedUpdateOrCreateChart 메서드가 실행되는 횟수가 648에서 1로 감소했습니다.


결과적으로 두 페이지 모두 TBT 수치가 약 150ms 가량 감소했으며, 성능 점수도 각각 69점에서 88점으로, 74점에서 94점으로 크게 상승했습니다.

리플로우(Reflow)를 유발하는 코드 제거
웹 페이지의 레이아웃이 변경될 때 브라우저는 다음 과정을 거칩니다.

- 레이아웃(Layout): 브라우저가 각 요소의 위치와 크기를 계산
- 페인트(Paint): 요소를 화면에 그림
- 복합(Composite): 여러 레이어를 결합해 최종 화면을 렌더링
이 중 레이아웃 단계가 리플로우에 해당하며, 브라우저가 CSS 규칙을 처리하고 페이지 내에서 요소 정렬과 제약 조건을 계산하는 복잡한 작업이 포함됩니다. 이 과정은 상당한 CPU 리소스가 사용되므로, 리플로우가 빈번하게 발생할수록 웹페이지 속도와 사용자 경험이 저하됩니다.
이러한 리플로우를 최소화하기 위한 다양한 방법이 있습니다.
- DOM 조작 최소화: DOM 변경 작업을 한 번에 묶어 배치(batch) 처리하거나, 브라우저의 유휴 상태(Idle Time)를 활용해 리플로우 최소화
- DOM 구조의 Depth 감소: 중첩된 DOM 요소의 구조를 단순화해 렌더링 과정에서 불필요한 계산을 최소화
- CSS 선택자 규칙 구체적 지정: 복잡한 CSS 선택자를 피하고, 명확하고 구체적인 규칙을 사용해 스타일 계산 비용을 절감
챗봇 관리자센터는 공통 컴포넌트에서 리플로우를 유발하는 속성(메서드)을 파악하고, 해당 부분을 중점적으로 개선했습니다.
자바스크립트를 통한 DOM 조작을 CSS로 대체하기
첫 번째 개선 대상은 서비스 내에서 공통으로 사용되는 InputTextComponent 컴포넌트입니다. 이 컴포넌트는 한 페이지 내에서 수십 개가 동시에 사용되기도 하는 주요 컴포넌트입니다.

// input-text.ts
import {Component, ElementRef, AfterViewInit, ViewChild} from '@angular/core';
@Component({
selector: 'input-text',
templateUrl: './input-text.html',
})
export class InputTextComponent implements AfterViewInit {
@ViewChild('inputEl', {static: true})
inputEl: ElementRef;
constructor(private element: ElementRef) {}
ngAfterViewInit(): void {
if (getComputedStyle(this.element.nativeElement).display \=== 'inline') {
this.element.nativeElement.style.display \= 'block';
}
}
}
InputTextComponent는 앵귤러의 라이프 사이클 훅인 ngAfterViewInit에서 getComputedStyle 메서드를 호출해 요소의 display 속성을 확인하고 변경하는 작업을 수행합니다. 그러나 getComputedStyle 메서드는 브라우저가 레이아웃 정보를 기반으로 DOM 요소의 실제 스타일 값을 계산해야 하므로, 이 과정에서 리플로우가 반복적으로 발생할 가능성이 있습니다.
브라우저는 일반적으로 DOM 변경 사항을 비동기적으로 처리하지만, getComputedStyle 메서드는 브라우저가 현재의 정확한 레이아웃 정보를 요구하기 때문에 대기 중인 모든 스타일 계산과 레이아웃을 즉시 처리해야 합니다. 이러한 동기적 처리로 인해 렌더링 파이프라인이 중단되고, 이는 곧 브라우저의 성능을 저하시키는 주요 원인이 됩니다.
Performance 탭을 통해 확인해 보면 페이지 내에서 사용된 모든 InputTextComponent 컴포넌트가 getComputedStyle 메서드를 반복해서 호출하며 성능 문제를 유발하고 있습니다. 특히 Force style recalculation이 과도하게 발생하고 있습니다.

getComputedStyle 메서드 대신 CSS 클래스를 통해 요소의 display 속성을 설정하도록 변경한 결과, Force style recalculation 작업이 완전히 제거된 것을 확인할 수 있었습니다. 이는 CSS를 통해 스타일을 변경할 경우 브라우저가 DOM과 CSSOM을 기반으로 최적화된 방식으로 스타일을 처리하기 때문에, 동기적인 스타일 계산과 리플로우가 최소화된 결과입니다.

불필요한 리플로우 유발 코드 제거하기
두 번째 개선 대상은 kakao-wrap-style 컴포넌트로, 개별 페이지 스타일을 위한 CSS 클래스 업데이트와 스크롤 처리를 담당하는 컴포넌트입니다.
// kakao-wrap-style.ts
import {DOCUMENT} from '@angular/common';
import {Component, HostListener, Inject, Input, OnInit} from '@angular/core';
import {WINDOW} from 'app/core/helpers/window.helper';
@Component({
selector: 'kakao-wrap-style',
template: '',
})
export class KakaoWrapStyleComponent implements OnInit {
@Input()
scrollMarker = false;
private enabledScrollOn = false;
constructor(@Inject(DOCUMENT) private document: Document, @Inject(WINDOW) private window: Window) {}
ngOnInit() {
// ...
this.checkScroll();
}
@HostListener('window:scroll', [])
onWindowScroll() {
// ...
this.checkScroll();
}
private checkScroll() {
if (!this.scrollMarker) {
return;
}
const MAX_POS_Y = 80;
const MIN_POS_Y = 10;
const posY =
this.window.pageYOffset || this.document.documentElement.scrollTop || this.document.body.scrollTop || 0;
if (posY > MAX_POS_Y) {
this.enabledScrollOn = true;
} else if (this.enabledScrollOn && posY < MIN_POS_Y) {
this.enabledScrollOn = false;
}
}
}

대부분의 페이지에서 사용되는 kakao-wrap-style 컴포넌트는 다음과 같은 방식으로 동작합니다.
- 스크롤 이벤트 감지 및 현재 스크롤 위치 계산: 윈도우(Window) 객체에서 스크롤 이벤트가 발생할 때마다 window.pageYOffset과 scrollTop 속성을 이용 현재 스크롤 위치를 계산합니다. scrollTop 속성은 브라우저 레이아웃의 정보를 가져오는 과정에서 리플로우를 유발해 성능 저하의 주요 원인이 됩니다.
- 스크롤 설정 업데이트: 현재 스크롤 위치를 기반으로 enabledScrollOn를 업데이트합니다.
- CSS 클래스 업데이트: updateClassName 메서드는 enabledScrollOn 값에 따라, 스크롤 설정을 위한 CSS 클래스를 추가하거나 제거합니다. 이 과정에서 페이지의 탑(top)과 마진(margin) 값이 변경되며, 리플로우가 발생합니다.


스크롤을 위한 스타일 변경은 이전과 같이 CSS 클래스를 이용하는 방식으로 해결할 수 있었습니다. 그러나 놀랍게도 챗봇 관리자센터에서는 이미 CSS를 중복으로 적용하고 있었고, 단순히 checkScroll 메서드를 제거하는 것만으로도 성능 개선 효과를 얻을 수 있었습니다.
많은 프로젝트에서 제대로 동작하지 않거나 불필요한 코드들이 리플로우를 유발하고 있을 가능성이 큽니다. 특히, 스크롤 관련 코드는 스크롤이 일어날 수 없는 조건에서도 계속해서 스크롤 이벤트를 감지하거나 처리하여 성능에 부정적인 영향을 미치는 경우가 많습니다. 따라서 프로젝트 내에서 불필요하게 리플로우를 유발하고 있는 코드들을 점검하고 개선하는 것을 권장합니다.
개선 이후 성능 측정 결과에서는 Recalculate Style 관련 작업이 모두 제거되었으며, 롱 태스크의 시간이 크게 감소한 것을 확인할 수 있었습니다.
머신러닝 페이지의 성능 측정 결과 비교
롱 태스크 시간이 약 116ms에서 46ms로 감소되어 롱 태스크에서 제외되었습니다.

관리자 페이지의 성능 측정 결과 비교
롱 태스크 시간이 약 126ms에서 47ms로 감소되어 롱 태스크에서 제외되었습니다.

이처럼 간단한 수정으로도 불필요한 성능 저하를 방지할 수 있습니다. 이는 리플로우를 유발하는 속성 사용 시 신중한 검토가 필요하다는 점을 보여주는 사례라고 할 수 있습니다.
전략적 라우트 프리로드(Route Preload)로 TBT 줄이기
챗봇 관리자센터 페이지들의 성능 측정 결과에서 한 가지 공통점을 발견할 수 있었습니다. 모든 페이지의 TBT 수치가 높고, 메인 스레드 작업 최소화가 필요하다는 가이드가 제시되고 있다는 점입니다. 심지어, 성능 점수가 90점 이상인 단순 안내 페이지들까지 TBT가 200-300ms 이상으로 측정되어, 공통적으로 개선 가능한 지점을 도출하고자 했습니다.

개선 포인트 탐색
챗봇 관리자센터의 메인 스레드에서 발생하는 작업을 분석해 보겠습니다. 아래는 챗봇 관리자센터 봇 목록 페이지의 성능 측정 결과입니다.


봇 목록을 보여주는 비교적 단순한 페이지임에도 불구하고, 긴 롱 태스크 구간이 포함되어 있습니다. 특히 노란색으로 표시된 스크립팅(Scripting) 구간이 빈번하게 나타나고 있어 해당 구간을 자세히 분석해 보았습니다.

스크립팅 구간에서는 앵귤러의 라우터(Router) 관련 메서드인 loadChildren, preload, processRoutes 등이 실행되고 있었습니다. 이 작업들은 앵귤러 애플리케이션의 라우팅 과정에서 발생하며, 라우트(Route) 데이터 처리 및 모듈 로드와 관련되어 있습니다.
라우팅 과정에서 처리되는 작업들이 성능에 어떤 영향을 미치는지 더 자세히 분석하기 위해, 챗봇 관리자센터의 라우팅 구성을 살펴보았습니다.
1. 라우트 별로 기능 모듈이 레이지 로드(Lazy load) 되도록 라우팅 구성
애플리케이션 초기 로드 시 필요한 코드만 로드하고, 특정 라우트로 이동할 때 필요한 코드를 동적으로 로드하는 방법입니다. 각 모듈을 별도의 번들로 분리하고 메인 번들의 크기를 줄임으로써 초기 로딩 속도를 개선하고, 사용자 경험을 향상 시킬 수 있습니다.
// app.routing.ts
import {Routes} from '@angular/router';
import {BotPageComponent} from './shared/pages/bot.page';
import {ErrorPageCode} from './constants';
const routes: Routes = [
{
path: 'policy',
loadChildren: () => import('./modules/policy/policy.module').then(m => m.PolicyModule),
},
{
path: 'notice',
loadChildren: () => import('./modules/notice/notice.module').then(m => m.NoticeModule),
},
{
path: 'bot',
component: BotPageComponent,
children: [
{
path: ':id/intent',
loadChildren: () => import('./modules/block/block.module').then(m => m.BlockModule),
},
{
path: ':id/training',
loadChildren: () => import('./modules/training/training.module').then(m => m.TrainingModule),
},
{
path: ':id/histories',
loadChildren: () => import('./modules/histories/histories.module').then(m => m.HistoriesModule),
},
{
path: ':id/manager',
loadChildren: () => import('./modules/manager/manager.module').then(m => m.ManagerModule),
},
],
},
{path: '**', redirectTo: `/error/${ErrorPageCode.NotFound}`},
];
2. preloadingStrategy를 PreloadAllModules로 설정
모듈 레이지 로드 방식의 단점은 라우트 탐색 시, 필요한 모듈을 로드하기 때문에 페이지 로드 속도가 느려질 수 있다는 것입니다. 이를 보완하기 위해 앵귤러에서 제공하는 preloadingStrategy 설정을 PreloadAllModules로 설정하면 레이지 로드 대상인 번들을 백그라운드에서 미리 로드하여 페이지 전환 속도를 높일 수 있습니다.
// app.routing.ts
export const routing = RouterModule.forRoot(routes, {
preloadingStrategy: PreloadAllModules,
scrollPositionRestoration: 'enabled',
});
두 방식의 조합은 성능과 사용성에 유리한 라우팅 구성을 제공할 것처럼 보이지만, 실제로는 preloadingStrategy 설정이 TBT가 증가의 주요 원인으로 작용하고 있었습니다.
앵귤러 공식 문서에서는 Preloading modules 전략이 백그라운드에서 모듈을 미리 로드해 사용성을 높인다고 설명하고 있습니다. 이는 초기 로드 성능에는 영향을 미치지 않을 것처럼 보입니다. 그럼에도 불구하고 빈번하게 롱 태스크가 발생하고 있어 이에 대한 원인을 좀 더 자세히 조사해 보았습니다.

출처: https://angular.dev/guide/ngmodules/lazy-loading#preloading-modules-and-standalone-components

출처: https://web.dev/articles/route-preloading-in-angular
Route preloading strategies in Angular의 PreloadAllModules에 대한 설명에서 힌트를 찾을 수 있었는데요, preloadingStrategy가 성능에 부정적인 영향을 미치는 이유를 정리하면 다음과 같습니다.
- 네트워크 사용량 증가
- 모듈 수가 많을 경우, 공격적인 사전 로딩으로 네트워크 리소스를 비효율적으로 사용하게 되어 네트워크 사용량이 증가할 수 있습니다.
- 특히, 백그라운드에서 모듈을 미리 로드하는 작업이 진입 페이지 로드 완료 후에 진행된다는 보장이 없어, 진입 페이지 성능에 영향을 미칠 수 있습니다.
- 메인 스레드 부하
- 미리 로드된 모든 모듈을 라우트 경로에 등록하는 과정에서 메인 스레드에서 집중적인 계산이 발생합니다.
- 이로 인해 브라우저의 렌더링이 지연되고, TBT 수치가 증가할 수 있습니다.

특히 [그림 48]의 공지사항 모듈과 같이 사용자의 방문 빈도가 낮지만 번들 크기가 큰 페이지들이 많을수록, 다른 페이지들의 성능이 저하되는 문제가 발생합니다. 때문에 PreloadAllModules 전략은 모듈의 수가 많은 대규모 애플리케이션에서는 적합하지 않은 옵션으로 보입니다.

개선 방향
문제의 원인을 파악했으니 사용성과 성능을 모두 만족시킬 수 있는 새로운 라우팅 전략을 구성해 보겠습니다.
1. NoPreloading 전략(Default)
// app.routing.ts
export const routing = RouterModule.forRoot(routes, {
preloadingStrategy: NoPreloading,
scrollPositionRestoration: 'enabled',
});
가장 단순한 방법으로, 기존의 PreloadAllModules 옵션을 제거하는 방식입니다. PreloadAllModules 제거 후 성능을 측정한 결과, 스크립팅 관련 작업의 비중이 줄어들고, 프리로드 관련 작업들이 전부 사라지면서 롱 태스크 시간이 272ms에서 243ms로 감소했습니다.


하지만 이 방법은 페이지 전환 속도가 느려질 수 있습니다. 특히, 번들 크기가 큰 페이지에 진입하거나 네트워크 환경이 좋지 않은 경우, 영상처럼 사용자가 체감할 수 있을 정도로 긴 딜레이가 발생합니다.
2. Custom Preloading 전략
서비스 특성, 페이지 방문 빈도, 모듈의 크기 등을 고려해 프리로드 환경을 구성하는 방법입니다. 가장 기본적인 Custom Preload Service를 만들어 선택적으로 라우트에 적용해 보았습니다.
// custom-preloading-strategy.service.ts
import {Injectable} from '@angular/core';
import {PreloadingStrategy, Route} from '@angular/router';
import {Observable, of} from 'rxjs';
@Injectable({
providedIn: 'root',
})
export class CustomPreloadingStrategyService implements PreloadingStrategy {
preload(route: Route, fn: () => Observable): Observable {
if (route.data && route.data.preload) {
return fn();
}
return of(null);
}
}
이 방법은 서비스 특성에 맞게 커스텀할 수 있고 별도의 패키지 없이 구현이 가능하다는 장점이 있습니다. 그러나, 프리로드 대상 모듈의 기준이 모호할 수 있으며, CustomPreloadingStrategyService를 좀 더 정교하게 구현하려면 브라우저의 유휴 상태, 네트워크 상태, 폴리필 등을 추가로 고려해야 합니다.
3. 페이지 전환 가능성이 있는 모듈만 프리로드하는 전략(QuickLink 전략)
QuickLink 전략은 페이지 내에서 사용자가 이동 가능한 동선, 즉 페이지 전환 가능성이 있는 모듈만 프리로드하는 방식입니다. 예를 들어 인터섹션 옵저버(Intersection Observer)를 사용해 라우팅 대상을 감지하고 해당 모듈만 프리로드하면 초기 페이지 로드 속도와 페이지 전환 속도에 모두를 개선할 수 있습니다.
특히 챗봇 관리자센터의 2가지 특성을 고려했을 때 QuickLink 전략이 가장 적합해 보입니다.
1. 다양한 권한 체계에 따른 메뉴 접근 제한
기존에는 접근 권한이 없어 렌더링 되지 않는 메뉴의 번들까지 로드했지만, QuckLink 전략으로 접근 가능한 메뉴의 번들만 로드해 불필요한 네트워크 사용량을 줄이고, 초기 로딩 속도를 개선에 기여할 수 있습니다.

2. 2 depth로 구성된 메뉴 구조
LNB 메뉴 중 ‘스킬’, ‘분석’ 메뉴는 하위 메뉴를 포함한 2 depth 구조로, 상위 메뉴가 접힌 상태로 로드됩니다. 특히 차트 라이브러리를 사용하는 ‘분석’ 모듈은 번들의 크기가 크기 때문에 QuickLink 전략을 통해 사용자가 상위 메뉴를 클릭하는 시점, 즉, 페이지 이동 가능성이 높아진 시점에 번들을 로드하도록 개선할 수 있습니다. 이를 통해 메인 번들의 부하를 줄이고, 다른 페이지의 로드 속도를 향상시킬 수 있습니다.

QuickLink 전략을 위해 CustomPreloadingStrategyService 같은 별도 서비스를 직접 구현하는 대신 ngx-quicklink 패키지를 사용하기로 결정한 이유는 아래와 같습니다.
- 최적화: 앵귤러 개발자가 제작한 패키지로, 앵귤러 프레임워크에 맞게 최적화됨
- 유지 보수: 지속적인 유지 보수 및 앵귤러 버전 업데이트에 대한 신속한 대응
- 경량화: 2KB로 경량화된 패키지 번들 크기
ngx-quicklink 패키지는 브라우저와 네트워크 상태까지 모두 고려해 효율적인 프리페칭(Prefetching)을 다음 프로세스를 통해 수행합니다.
- 뷰포트 내 라우터 링크 감지: Intersection Observer를 사용해 사용자의 화면에 진입한 라우터 링크를 감지
- 브라우저 유휴 상태 대기: 브라우저가 유휴 상태(Idle)가 될 때까지 requestIdleCallback을 사용해 대기 (메인 스레드의 부하를 최소화하기 위해)
- 네트워크 상태 확인: navigator.connection.effectiveType API를 사용해 사용자가 느린 연결 상태에 있지 않은지 확인. navigator.connection.saveData API를 사용해 데이터 세이버가 활성화 여부 확인
- 앵귤러 프리페칭 전략 활용: 앵귤러의 프리페칭 전략을 사용해 레이지 로드 모듈을 프리페칭
// app.routing.ts
import {QuicklinkStrategy} from 'ngx-quicklink';
export const routing = RouterModule.forRoot(routes, {
preloadingStrategy: QuicklinkStrategy,
scrollPositionRestoration: 'enabled',
});
사용법도 매우 간단합니다. preloadingStrategy를 QuickLinkStrategy로 설정하면 즉시 적용됩니다. 또한 특정 경로의 data 속성에 preload:false를 설정해 해당 모듈을 프리로드 대상에서 제외할 수도 있습니다.
// app.routing.ts
const routes: Routes = [
{
path: 'notification',
component: AccountNotificationPageComponent,
data: {preload: false},
children: [
// ...
],
},
];
QuickLink 전략 적용 후, 시나리오 페이지 진입 시 기본 main 번들과 LNB에서 이동할 수 있는 페이지들의 번들이 로드되고, 접혀있던 분석 메뉴가 오픈될 때 통계, 인사이트 모듈이 로드됩니다.
<그림 54. QuickLink 전략 적용 후 분석 메뉴 로드 과정>
네트워크 탭에서 좀 더 자세한 차이를 확인할 수 있습니다. 기존의 PreloadAllModules 설정에서는 첫 페이지 진입 시 라우팅 경로에 등록된 모든 모듈을 다운하고 있습니다.
<그림 55. 기존 PreloadAll 전략에서의 네트워크 탭>
반면 QuickLink 전략을 사용 시에는 공통으로 사용하는 main.js와 shared 모듈을 우선적으로 다운로드 후, 진입한 페이지의 모듈 > 그리고 그 외 LNB 메뉴들의 모듈이 순차적으로 다운로드되고 있습니다.
<그림 56. QuickLink 전략 적용 후 네트워크 탭>
이번에는 Performance 탭에서 두 설정을 비교해 보겠습니다. PreloadAllModules을 사용할 경우 TTI 이전 시점에 프리로드 대상 모듈의 라우트 등록과 스크립트 파싱이 진행되며 롱 태스크가 증가하고 있습니다.
<그림 57. 기존 PreloadAll 전략에서의 Performance 탭>
QuickLink 전략 사용 시, 당장 필요하지 않은 프리로드 대상 모듈의 스크립트 파싱이 TTI 이후로 지연됩니다. 이를 통해 롱 태스크 구간을 줄이고 TBT 개선에 기여할 수 있습니다.
<그림 58. QuickLink 전략 적용 후 Performance 탭>
2.3.3 개선 결과
QuickLinkStrategy 적용 후 대부분의 페이지에서 ‘자바스크립트 실행 시간 단축’, ‘기본 스레드 작업 최소화하기’ 항목의 시간이 감소하고 성능 점수가 약 5~10점씩 상승했습니다.

봇 설정 > 챗봇 관리 페이지 성능 개선 결과
TBT 수치가 약 440ms에서 200ms로 감소하며 성능 점수가 약 15점 이상 상승했습니다.

봇 설정 > 지식 베이스 정보 페이지 성능 개선 결과
TBT 수치가 약 340ms에서 150ms로 감소하며 성능 점수가 약 10점 이상 상승했습니다.

PreloadAllModules에 대한 간략한 설명만 보면 큰 고민 없이 적용할 수 있는 유용하고 간단한 옵션처럼 보입니다. 하지만 실제로는 프리로드에 대한 대가를 톡톡히 치르고 있었습니다. 이번 경험을 통해 프레임워크나 라이브러리에서 제공하는 기능을 사용할 때는 동작 방식과 잠재적인 사이드 이펙트를 더욱 신중히 검토하는 것이 얼마나 중요한지 다시 한번 느꼈습니다. 특히 개발 중인 애플리케이션의 특성에 따라 의도치 않은 문제가 발생할 수 있으므로, 해당 기능이 애플리케이션에 적용하기 적합한지 여러 측면에서 꼼꼼히 검토하는 것이 중요할 것 같습니다.
트러블 슈팅 - CLS 개선 작업 중 발생한 DOM 업데이트 문제
문제 현상
CLS 개선 작업에서 kakao-content-style 컴포넌트 개선이 완료되었다고 판단했지만, 예상치 못한 문제가 발생했습니다. kakao-content-style 컴포넌트의 ngAfterViewInit 메서드를 통해 각 페이지에 필요한 클래스를 추가하도록 구현했음에도, 특정 상황에서만 클래스가 제대로 추가되지 않아 스타일이 적용되지 않는 현상이 발견되었습니다.
<그림 62. 상위 라우트에서 하위 라우트로 이동 시, 클래스가 추가되지 않는 문제가 발생하고 있다>
페이지의 URL로 직접 진입하거나 LNB의 버튼을 통해 페이지에 진입할 경우, 각 페이지의 #kakaoContent에 클래스가 정상적으로 추가되고 있습니다. 하지만 내 월렛 페이지에서 LNB의 하위 페이지로 진입할 때만 클래스가 정상적으로 추가되지 않는 문제가 발생하고 있었습니다.
원인 분석
<그림 63. 내 월렛 페이지에서 관리자 페이지로 이동할 때 컨테이너 요소에 클래스가 의도대로 추가되지 않아 관리자 페이지의 스타일이 적용되지 않고 있다>
내 월렛 페이지에서 관리자 페이지로 이동할 때 DOM 변화를 디버깅한 결과 다음과 같은 순서로 DOM이 업데이트되고 있었습니다.
- 관리자 페이지 컴포넌트()가 DOM에 추가
- 관리자 페이지의 초기화 & kakao-content-style 컴포넌트 초기화
- 내 월렛 컴포넌트()가 DOM에서 제거
예상한 DOM 업데이트 순서는 3 > 1 > 2였지만, 실제로는 1 > 2 > 3 순서로 진행되고 있었습니다. 즉, 일시적으로 DOM에 두 개의 #kakaoContent 요소가 존재하는 상황이 발생하고 있었습니다. kakao-content-style 컴포넌트는 document.querySelector를 이용하고 있었기 때문에, 첫 번째 #kakaoContent(내 월렛 페이지의 컨테이너)에 관리자 페이지의 클래스가 추가되는 문제가 발생하게 된 것입니다.
내 월렛 페이지의 ngOnDestroy 훅과 관리자 페이지의 ngOnInit 훅의 실행 시점을 비교한 결과, ngOnDestroy가 먼저 실행된 뒤 gOnInit이 실행되고 있었습니다. 앵귤러 라이프 사이클 훅은 보통 컴포넌트 초기화 순서에 따라 실행되므로, 이 과정이 DOM 업데이트와도 일치할 것이라고 생각했습니다. 하지만 실제로는 라이프 사이클 훅의 실행 시점과 DOM 업데이트 타이밍이 불일치하고 있었습니다.
앵귤러의 이슈 페이지에서 비슷한 사례들을 참고한 결과, 문제의 원인은 BrowserAnimationModule로 밝혀졌습니다. BrowserAnimationModule은 앵귤러 애플리케이션의 애니메이션을 구현하기 위해 사용하는 모듈로 브라우저에서 애니메이션을 처리하기 위한 기능을 제공합니다. 이 모듈은 DOM 변경 감지가 완료될 때까지 기존 요소를 DOM에 유지하며, 이후 제거하도록 동작합니다. 이로 인해 라이프 사이클 훅 실행 시점과 DOM 업데이트 타이밍 간에 차이가 발생하는 것입니다.

예시 코드를 통해 BrowserAnimationModule의 동작을 좀 더 쉽게 이해할 수 있습니다. Update 버튼 클릭 시, DOM에는 변경된 데이터(4,5,6)가 출력되지만 콘솔에는 이전 데이터(1,2,3,4,5)가 출력됩니다. 하지만 BrowserAnimationModule을 제거한 뒤 실행해 보면, DOM과 콘솔에 동일한 데이터(4,5,6)가 출력되는 것을 확인할 수 있습니다. 이를 통해 BrowserAnimationModule이 DOM 변경 타이밍에 영향을 미친다는 것을 확인할 수 있습니다.
정리하자면, BrowserAnimationModule 사용 시, 기존 요소가 제거되고 새로운 요소가 추가될 때, 새로운 요소는 즉시 DOM에 추가됩니다. 그러나 변경 감지가 완료될 때까지 기존 요소가 DOM에 남아있을 수 있습니다. 새로운 요소가 DOM에 추가되면 앵귤러는 해당 요소에 대한 변경 감지와 초기화를 수행합니다. 이로 인해 기존 요소가 DOM에 남아있는 상태에서 새로운 요소의 라이프 사이클 훅이 실행되며, 결과적으로 예상과 다른 방식으로 DOM 변경이 수행될 수 있습니다.
문제 해결
챗봇 관리자센터도 BrowserAnimationModule를 사용하고 있기 때문에, 새로 라우팅 되는 페이지의 라이프 사이클 훅이 실행될 때, 기존 페이지가 DOM에 남아 있는 상태였습니다. 이로 인해 기존 페이지의 컨테이너 요소(#kakaoContent)에 클래스가 잘못 추가되는 문제가 발생하고 있었던 것입니다. BrowserAnimationModule를 유지하면서 각 페이지별 스타일을 정상적으로 적용하기 위해, 챗봇 관리자센터의 페이지 구조를 고려해 기존 코드를 수정했습니다. 각 페이지와 #kakaoContent 요소는 1대 1 관계를 가지므로, kakao-content-style 요소와 가장 가까운 #kakaoContent 요소에 클래스를 추가하도록 변경했습니다. 이 방식은 DOM이 비동기적으로 업데이트되더라도, 각 페이지의 컨테이너에 클래스가 정확히 적용되도록 보장합니다.
// kakao-content-style.ts
import {Component, Input, Renderer2, ElementRef, AfterViewInit} from '@angular/core';
@Component({
selector: 'kakao-content-style',
templateUrl: './kakao-content-style.html',
})
export class KakaoContentStyleComponent implements AfterViewInit {
@Input()
private contentClass: string;
constructor(private renderer: Renderer2, private el: ElementRef) {}
ngAfterViewInit() {
const contentElement = this.el.nativeElement.closest('#kakaoContent');
if (contentElement) {
this.renderer.addClass(contentElement, this.contentClass || '');
}
}
}

BrowserAnimationModule은 앵귤러 애플리케이션에서 애니메이션을 지원하기 위해 흔히 사용되는 모듈이지만, 이 모듈로 인해 애플리케이션이 예상과 다르게 동작할 수 있다는 점은 놀라운 경험이었습니다. 이번 작업에서는 문제를 해결하기 위해 가장 사이드 이펙트가 적고 현실적인 방법을 채택했지만, document.querySelector 와 같은 직접적인 DOM 접근은 예상치 못한 문제를 유발할 가능성이 높다는 점을 다시 한번 느꼈습니다. 가능하다면 이러한 방식을 지양하고 라이브러리/프레임워크가 제공하는 템플릿 및 데이터 바인딩 방식을 적극 활용하는 것이 안정적이고 유지보수가 쉬운 코드를 작성하는데 도움이 될 것입니다.
성능 개선 결과
주요 개선 사항 요약
- CLS 개선
- 데이터 로드에 의한 레이아웃 변경 제거: ResolverFn를 활용해 라우팅 시 데이터를 미리 로드하도록 개선.
- 비동기적 DOM 업데이트 제거: 앵귤러의 생명주기 훅을 활용해 DOM 초기화 후 스타일을 안정적으로 업데이트하도록 변경.
- TBT 개선
- 컴포넌트 레이지 로드: 앵귤러의 @defer를 활용해 뷰포트에 포함되는 컴포넌트만 우선적으로 로드하여 초기 로드 시간 단축.
- 리플로우를 유발하는 속성 제거: 불필요한 리플로우를 유발하는 코드 제거 및 최적화.
- ngx-quicklink를 활용한 모듈 프리로드: 뷰포트에 포함된 진입 가능한 페이지의 번들만 프리로드해 네트워크 리소스 최적화.
평균 성능 점수 변화
챗봇 관리자센터의 평균 성능 점수가 81점에서 91점으로 약 10점 상승했습니다. 모든 페이지의 성능이 80점 이상으로 상승함에 따라 팀의 공통 목표도 달성하게 되었습니다.

주요 페이지별 성능 점수 변화
LNB의 주요 메뉴 페이지들의 성능이 모두 85점 이상으로 개선되어 프로젝트 초기에 설정했던 정량적 목표도 달성했습니다.
| 페이지 | 개선 전 | 개선 후 | 점수 변화 |
|---|---|---|---|
| LNB > 시나리오 | 69 | 93 | +24 |
| LNB > 봇 설정 > 지식베이스 정보 | 69 | 90 | +21 |
| LNB > 봇 설정 > 챗봇 관리 | 70 | 88 | +18 |
| LNB > 봇 머신러닝 | 76 | 93 | +17 |
| LNB > 봇 관리자 | 77 | 93 | +16 |
| LNB > 봇 스킬 > 오류 내역 | 79 | 94 | +15 |
| 봇 목록 | 80 | 94 | +14 |
| LNB > 봇 학습 | 80 | 94 | +14 |
| LNB > 봇 분석 > 인사이트 > 사용자 | 80 | 91 | +11 |
| LNB > 봇 분석 > 통계 대시보드 | 78 | 88 | +10 |
| LNB > 봇 스킬 | 82 | 91 | +9 |
| LNB > 봇 배포 | 84 | 92 | +8 |
| LNB > 봇 작업이력 | 83 | 90 | +7 |
맺으며
지금까지 약 2달간 진행한 챗봇 관리자센터 성능 최적화 과정을 공유해 보았습니다. 개인적으로 이번 프로젝트는 서비스와 프레임워크에 가장 적합한 최적화에 대해 고민할 수 있었던 좋은 경험이었습니다. 보편적인 웹 성능 최적화 방식에서 한 걸음 나아가, 서비스의 특성과 사용 중인 라이브러리 및 프레임워크의 특성을 자세히 분석하여 더욱 효과적인 개선 방안을 도출할 수 있었습니다. 앵귤러 프레임워크는 사용 사례가 많지 않고, 특히 최신 버전에서의 성능 최적화에 대한 레퍼런스가 부족해 기술적 난관을 겪기도 했는데요, 제 경험이 비슷한 고민을 하고 있는 개발자분들께 조금이나마 도움이 되길 바라며 글을 마칩니다.
마지막으로 함께 고민하고 피드백해 주신 reilly.o, frey.ryu, nina.oh, alvin.chip, wes.lee에게도 감사를 전합니다!
Edited by marron.b