여러 탭을 열면 느려진다는 피드백에서 시작했다

여러 탭에서 편집이 버벅일 때는 렌더링 비용과 함께 조회를 반복하는 이유부터 살폈다. 작업이 없거나 보지 않는 탭의 폴링, 넓은 캐시 무효화, 중복 상태 수신을 정리했다. CPU 4배 감속·Slow 4G에서 편집기 6개 탭을 10분 관찰한 단일 전후 실행에서는 탭 평균 Long Task가 136개에서 4.7개로, 총 블로킹 시간이 10.85초에서 0.57초로 줄었다.

출발점은 AI Commons의 강의 슬라이드 편집기를 여러 탭으로 열면 PC가 느려지고 편집 동작이 버벅인다는 피드백이었다. React 렌더링에 조사 범위를 가두지 않고, 화면의 조회가 서버를 거쳐 캐시와 컴포넌트를 갱신하는 경로를 함께 확인했다. 사용자 입력이 없는 동안에도 일이 발생하는지를 먼저 살폈다. 여러 변경을 함께 적용한 비교라 각각의 기여도는 분리하지 못했다.

조회 간격보다 조회할 이유를 먼저 줄였다

기존 작업 상태 조회에는 React Query 밖의 setInterval이 사용됐다. 탭을 보고 있지 않아도 5초마다 요청했고, 처리할 작업이 없는 경우에도 조회가 이어졌다. 여러 탭에서 같은 패턴이 반복되면 각 탭의 갱신 비용과 서버 요청이 함께 쌓인다. 화면이 멈춰 보이는 문제를 살필 때 백그라운드 탭도 같은 작업을 하는지 확인해야 했던 이유다.

폴링은 refetchInterval로 옮기고 실제 생성 작업이 진행 중인 조건에서 동작하도록 바꿨다. 비활성 탭의 불필요한 폴링도 중단했다. 단순히 요청 간격을 길게 늘리면 진행 중인 작업의 상태 표시까지 늦어질 수 있다. 작업이 없을 때 요청하지 않는 조건과 작업 중에 필요한 갱신 주기를 구분하는 편이 제품 동작을 설명하기 쉬웠다.

// 현재 편집기 폴링 조건을 줄인 형태
refetchInterval: (query) => {
  if (batchJobInProgressRef.current) return 5000;
  const jobs = query.state.data?.activeJobs ?? [];
  return jobs.some(j => ["QUEUED", "PROCESSING"].includes(j.status))
    ? 5000 : false;
}

현재 코드는 위 조건에 임포트·내보내기 상태와 단건 activeJob 호환도 포함한다. 당시 측정의 코드를 그대로 복원한 발췌는 아니다. 진행 중인 작업을 놓치지 않는 조건을 유지하면서, 완료 뒤에는 같은 조회가 반복되지 않게 하는 방식이다.

데이터를 수정한 다음의 캐시 무효화 범위도 좁혔다. 슬라이드의 일부를 바꿨는데 과목과 다른 리소스까지 다시 조회하면, 한 번의 조작에서 관련 없는 갱신이 이어진다. 공통 훅에서 넓게 무효화하기보다 사용처가 필요한 범위를 지정하도록 바꿨다. 캐시를 남겨 두기만 하는 최적화가 되지 않도록, 변경한 데이터가 화면에 반영되는 경로는 유지해야 했다.

렌더링과 서버의 조회 경로를 함께 정리했다

슬라이드 편집기에서는 매번 새로 만들어지는 핸들러와 배열 참조를 정리하고, React.memo와 비교 함수를 적용했다. 부모가 갱신됐다는 이유만으로 바뀌지 않은 하위 화면까지 다시 그리는 일을 줄이려는 조치였다. 다만 메모이제이션 자체가 성능의 증거는 아니다. 비교 비용과 실제 변경 사항의 반영 여부를 함께 살펴야 한다.

서버의 작업 상태 조회에는 서로 다른 종류의 Job을 차례로 읽는 구간이 있었다. 독립적으로 조회할 수 있는 부분은 Promise.all로 묶었다. 동시에 SSE와 폴링으로 같은 작업 상태를 중복해서 받는 경로도 정리했다. 브라우저 안에서 보이는 지연을 추적하더라도, 그 갱신을 시작시키는 요청과 응답의 수를 같이 봐야 했다.

  1. 상태 요청이 반복됨작업이 없는 탭과 비활성 탭에도 불필요한 조회가 이어졌다.
  2. 응답과 캐시 무효화가 갱신을 유발함관련 없는 리소스까지 다시 조회하고, 새 참조가 하위 컴포넌트 갱신으로 이어질 수 있었다.
  3. 여러 탭이 같은 일을 수행함네트워크 왕복과 응답 처리·렌더링이 겹치며 입력을 처리할 메인 스레드의 여유를 줄였다.
  4. 갱신을 시작시키는 조건부터 줄임필요한 폴링만 남기고 무효화·참조·상태 수신 경로를 함께 정리했다.

이 변경들을 브라우저의 Long Task 하나에 직접 대응시키지는 않았다. 서버 조회를 병렬화한 효과와 컴포넌트 갱신을 줄인 효과는 다른 경로에서 발생한다. 여러 변경을 함께 적용한 뒤 전체 프로파일이 달라졌다는 사실은 확인할 수 있지만, 그 결과만으로 각 변경의 기여도를 계산할 수는 없다.

같은 6탭·10분 조건으로 다시 측정했다

Chrome DevTools에서 CPU를 4배 감속하고 Slow 4G 네트워크 조건을 적용했다. 슬라이드 편집기 6개 탭을 열고 전체 이미지 생성을 함께 실행하며 10분 동안 관찰했다. 아래 값은 탭별 지표의 평균이다. 6개 탭의 합계나 초당 발생 수로 읽으면 다른 결과가 된다.

탭 평균 Long Task
이전
136개
이후
4.7개
0에서 시작 · 지표별 이전 값을 100%로 비교
탭 평균 총 블로킹 시간
이전
10.85초
이후
0.57초
0에서 시작 · 지표별 이전 값을 100%로 비교
6개 탭 · 10분 · CPU 4× · Slow 4G · 각 탭의 평균값
지표 · 탭 평균개선 전개선 후
Long Task 수136개4.7개
총 블로킹 시간10.85초0.57초

Long Task는 메인 스레드가 한 작업에 길게 묶인 구간을 살피는 지표다. 개수만 줄었어도 남은 작업이 길어졌다면 입력에 응답할 여유가 충분해졌다고 보기 어렵다. 그래서 이 프로파일에서는 개수와 블로킹 시간을 함께 기록했다. 두 값은 서로 다른 실험의 성과가 아니라 같은 측정에서 나온 결과다.

큰 감소폭보다 무엇을 비교했는지를 남긴다

이번 결과는 여러 변경을 묶어 적용한 단일 통제 실행의 전후 비교다. 원래 DevTools trace 파일은 현재 보관 근거에 남아 있지 않아, 가장 큰 콜백이나 특정 flame chart 구간을 다시 제시할 수 없다. 보관된 측정값과 수정 경로가 근거다. CPU 감속은 실제 저사양 기기 전체를 대표하지 않으며 반복 편차와 변경별 기여도도 분리하지 못했다.

후속 측정에서는 변경을 나눠 프로파일을 남기고, 활성 탭과 비활성 탭의 요청 수를 따로 확인해야 한다. 실제 기기에서 편집과 생성이 겹칠 때의 입력 반응도 비교할 필요가 있다. 이번 작업에서는 불필요한 작업을 반복하는 조건부터 확인했다. 렌더링 비용을 줄이는 일과 함께, 그 렌더링과 서버 조회를 반복해서 일으키는 조건을 먼저 줄여야 했다.

제품 전체의 맥락은 AI Commons 프로젝트에서 볼 수 있다. 지표의 기술적 정의는 MDN의 PerformanceLongTaskTiming 설명을 참고할 수 있다.