타입스크립트의 타입 추론이 점점 느려져 답답하던 중,
당근 테크 밋업에서 소개된 방법으로 타입 추론의 병목 지점을 찾아
해당 부분만 수정함으로써 속도를 빠르게 개선할 수 있었어요.
그 경험을 정리한 글이에요.
인텔리 센스, 그것이 느려지는 이유
인텔리센스는, IDE가 제공하는 코딩 편의기능이에요.
코드를 타이핑할 때에 사용할 수 있는 문법들을 보여주고,
타입체크도 해주어 잘못작성한 문법을 지적해 주지요.
VSCode(이하 IDE)에서 TypeScript (이하 TS) 기반 개발을 하다 보면,
간혹 인텔리센스(IntelliSense)가 느려지는 경험을 할 때가 있어요.
IDE의 TS는 코드를 입력할 때마다 TS컴파일러가 코드를 분석하고 타입을 추론해 줘요.
이 과정이 실시간으로 이루어지기 때문에, 프로젝트의 코드 규모가 커질수록,
타입추론연산이 복잡한한 타입을 사용할수록, 인텔리센스의 반응속도가 저하되어요.
인텔리센스의 반응 속도가 약간 느려지면 그저 조금 답답한 정도에 그치지만,
그 정도가 심해지면 내가 현대적인 IDE와 TS를 사용하고 있는 것이 맞나 의심이 들 정도로 느려져요.
타입 보호나 자동 완성이 실행되는 것이 제가 코딩을 하는 것보다 느려져서
마치 VS Code 모양의 메모장에서 순수 JavaScript로 개발하는 듯한 느낌이 든답니다.
TS 추론 성능을 높이는 법
그러니 당연히 TS 개발자들에게 타입 추론 성능은 중요한 이슈랍니다.
TS의 개발사인 Microsoft는 개발자들에게 효율적인 타입 작성법을 알려주는
TypeScript 성능 최적화 가이드 위키 페이지를 관리하고 있어요.
또 "이펙티브 타입스크립트"와 같은 기술 서적에서도 추론 성능을 개선시키기 위한 방법들을 찾아볼 수 있지요.
대체로 동적으로 추론되는 타입을 줄이고, 되도록 명시적인 타입을 많이 사용하는 방법을 추천한답니다.
문제의 핵심
제가 겪은 문제의 핵심은 고쳐야 할 부분을 찾는 것이었어요.
성능저하 자체는 타입을 고치면 간단하게 해결되는 것이지만
코드 중 어떤 부분에서 타입 추론이 오래 걸리는지 알 수 없다는 점
즉, TS컴파일러에게 시간을 빼앗는 병목 지점, 혹은 핫스팟을 찾아내는 것이 진짜 문제였답니다.
저는 타입 추론 성능의 중요성에 대해 잘 인지하고 있었기에, 이를 의식하며 타입을 작성하고 있었어요.
그렇기에 타입추론이 점차 느려지자, 어디를 어떻게 수정해야 할지 더 막막한 기분이 들었어요.
해결 방법
어디가 문제일까 고민을 하던 와중, 최근에 참여했던 당근마켓 테크밋업에서
TS 타입추론의 성능 병목지점을 추적하는 여러 가지 툴을 소개하는 발표를 들었던 것이 기억이 났어요.
김은수 연사님의 “내 타입스크립트 이렇게 느릴 리 없어” 세션이었죠.
타입 추론과정에서 소요된 시간을 분석한 파일을 생성한 후, GUI도구에 해당 파일을 업로드하여
컴파일시간에 병목현상이 발견하는 곳, 이른바 핫스팟 을 찾아서, 그 부분의 타입을 고치는 방법 대해 소개를 해주셨었어요.
정확히 어떤 툴들을 소개해주셨었는지는 기억이 안 났지만, 다행히도 당근 테크밋업 공유노션에
발표 자료를 업로드해주셔서, 그것을 보고 따라 하여 병목 지점을 찾아 해결할 수 있었답니다.
정확히 제가 거친 방법은 다음과 같아요.
1. 컴파일 성능 분석 파일생성
2. GUI 성능 분석 서비스 페이지에 생성된 파일 업로드
3. 병목구간에 있는 파일 추적
4. 해당 파일의 타입 코드 수정
이제 이 과정을 자세히 풀어보도록 할게요.
1. 컴파일 성능 분석 파일생성
컴파일 성능분석 파일생성을위해서는 tsc명령어를 --generateTrace플래그와 함께 사용하면 되어요.
입력한 경로에 컴파일 수행시간 정보가 담긴 trace.json 파일이 생성되지요.
tsc --generateTrace "원하는 경로"
2. GUI 성능 분석 서비스 페이지에 생성된 파일 업로드
생성된 trace.json 파일을 Google이 개발한 GUI 성능분석 서비스인 Perfetto UI에 업로드하면
다음과 같은 성능분석 그래프를 볼 수 있어요.
중간 부분에 위치한 얇은 바는 크게 4가지 색상으로 되어있는데, 이는 컴파일의 각 단계를 의미해요.
그중 3번째 진녹색 바는 checkSource단계의 작업 정보를 보여주는데,
이게 바로 제가 필요로 한 부분이에요. 타입스크립트의 타입 검사의 성능을 표시하는 구간이지요.
이 바를 확대해보면 같은색 길고 짧은 여러 개의 작은 바들의 구성으로 되어있는것을 살펴볼수있어요.
각각의 바는 하나의 파일을 의미해요. 이를 통해 개별 파일에 대한 작업수행 정보를 볼 수 있지요.
그 길이는 타입 검사 수행시간에 비례하는데,
여기서 다른 바에 비해 유난히 긴 것들을 골라서 해당 파일의 타입을 수정해주면 작업이 끝나요.
3. 병목구간에 있는 파일 추적
저는 이미 최적화를 완료했지만, 어떤 과정으로 탐색하는지 글로 남기기 위해
추가적인 작업을 해보기로했어요.
Perfetto에서 GUI그래프를 살펴보다가 주변의 다른 막대보다 유난히 긴 막대를 찾았어요.
useFilterOption이라는 커스텀 훅에대한 코드를 보관하고 있는 파일이에요.
이렇게 찾은 파일이 바로 저의 "표적"이에요. 해당 파일에대한 코드를 읽고
병목을 일으킬만한 동적 타입들을 찾아낸후, 적절하게 타입을 변경하면 작업이 끝나요
4. 해당 파일의 타입 코드 수정
이다음부터는 훅에 대한 소개를 하고, 어떤 근거로 생각을 하는지 쓸 예정이에요.
궁금하신 분은 조금 더 읽어보세요.
useFilterOption훅은 GET 요청에 사용할 파라미터인 filter배열의 상태를 관리하는 역할을 해요.
훅의 반환값으로 제공하는 메서드를 사용하여,
특정 값이 현재 filter에 포함되어 있는지 확인하거나, filter 포함 여부를 토글 할 수도 있지요.
웹 페이지의 게시판에서, 사용자가 버튼이나 체크박스를 눌러
다양한 카테고리로 조회 목록을 필터링할 수 있는 기능이 있는 서비스를 본 적이 있나요?
바로 그런 "필터링 설정 값"을 상태를 관리하는 것이랍니다.
(사진의 버튼들은 설명과는 조금 다른 동작을 해요. 궁금하신 분은 서비스 앱을 다운로드하여보세요)
이 훅은 다음과 같은 입력타입과 반환타입을 가지고 있어요.
export interface IUseFilterOptionsParameter<T extends readonly string[]> {
statusFilterOptions?: T;
}
interface IUseFilterStatesReturn<T extends readonly string[]]> {
/** 활성화된 상태 필터들의 배열, request param으로 사용됨 */
activeStatusFilterOptions: FilterOptionsType<T>[];
/** 특정 상태 필터의 활성/비활성 토글 핸들러 생성 */
getStatusStateToggleHandler: (status: FilterOptionsType<T>) => MouseEventHandler;
/** 특정 상태 필터가 활성화되었는지 확인하는 함수 */
checkIsActiveStatusFilter: (status: FilterOptionsType<T>) => boolean;
/** 모든 상태 필터를 활성화하는 함수 */
activateAllStatusFilters: () => void;
/** 전달된 상태 필터배열을 활성화하는 함수 (주로 초기화 용도) */
activateSomeStatusFilters: (activeOptions: FilterOptionsType<T>[]) => void;
/** 모든 상태 필터를 비활성화하는 함수 */
deactivateAllStatusFilters: () => void;
}
반환된 메서드의 입력값으로 사용되는 status
가
처음 훅을 초기화할 때 사용된 statusFilterOptions
배열에 포함되지 않는 경우,
타입 에러가 발생하게 만들어져 있답니다.
const memberFilterOptions = ["active", "inActive"] as const
const {checkIsActive} = useFilterOptions(filterOptions)
// 존재하지 않는값에대한 타입오류 발생
checkIsActive("superActive")
// filterOptions의 값을 사용하면 타입오류가 발생하지 않음
const onClick활성Button = getStatusStateToggleHandler("active")
아마도 이런 메서드들의 타입이 문제인 것 같았어요.
특히 getStatusStateToggleHandler처럼
핸들러 메서드를 반환하는 메서드들이 의심이 되었어요.
반환 타입이 다소 복잡한 편인데, 명시적으로 타입을 주지 않았고, 대부분 TS에게 직접 추론을 맡겼거든요.
반환값에 대한 타입을 한번 지정한 후에, tsc --generateTrace 명령어를 다시 사용하여 성능분석을 하자
다음과 같은 결과를 얻을 수 있었어요.
23ms에서 8ms로 줄어든 모습을 볼 수 있지요?
저는 이러한 방법으로 이 병목지점을 선택적으로 찾아서
Perfetto가 표시하는 타입 추론 시간을 1/7 미만으로 줄였어요.
여기서 표시되는 시간이 정확히 IDE에서의 정보 토글시간과 1:1로 대응하는지는 모르겠지만,
2초 정도 기다려야 볼 수 있던 정보를, 타이핑과 동시에 볼 수 있게 되었답니다.
비슷한 이슈에 대한 더 많은 경험이 보고 싶다면,
당근 유튜브 채널에서 김은수 연사님의 발표를 직접 시청해 보시길 바라요.
https://www.youtube.com/watch?v=g9FL8hKoNqE
감사하는 마음으로 당근 테크 블로그체로 작성하려고 해 보았습니다.
누가 읽어도 편안하게 둥글둥글한 문체를 만드는 것은, 쉽지 않은 작업이군요.
평소 게시물 저자분들에게 감사하는 마음이 있었는데, 존경심도 조금 생기는 경험이었습니다.