Next.js 앱에서 클릭이 한 박자 느리게 느껴질 때 살펴본 것들

서버가 멀면 응답이 느린 건 어쩔 수 없습니다. 제 경우 오리진 응답 지연에 CDN 우회 경로까지 겹쳐 페이지 응답에 1초 가까이 걸리는 상황이었고, 네트워크 쪽은 당장 손댈 수 없었습니다. 그래서 응답 속도 대신 "눌렀을 때 반응하는 속도"를 올리는 쪽으로 방향을 잡았습니다. 느린 것과 느리게 느껴지는 것은 생각보다 다른 문제였습니다.

처음에는 버튼 컴포넌트를 의심했습니다. 그런데 디자인시스템 버튼에는 이미 active:scale-95가 들어 있었고, 서버 액션을 부르는 버튼들도 누르는 즉시 스피너가 뜨게 돼 있었습니다. 버튼 탓이 아니었습니다. 한참 눌러보다가 찾은 구멍은 세 군데였습니다.

클릭하고 아무 일도 안 일어나는 페이지

loading.tsx가 없는 라우트가 세 개 있었습니다. Next.js 앱 라우터는 라우트에 loading.tsx가 있으면 이동하는 순간 스켈레톤을 먼저 그려주는데, 이게 없는 페이지는 클릭 후 서버 렌더가 끝날 때까지 화면에 아무 변화가 없습니다. 하필 그중 하나가 three.js를 불러오는 3D 페이지라 청크만 876KB였습니다. 클릭, 1초 정지, 화면 전환. "버튼이 늦게 눌린다"는 느낌의 절반은 여기서 나왔던 것 같습니다.

스켈레톤 파일 세 개를 추가하는 게 이번 작업에서 가성비가 제일 좋았습니다. 늦게 뜨는 건 그대로인데, 클릭한 순간 화면이 반응하니까 체감이 다릅니다.

눌렀다는 확인이 없는 링크

사이드바 링크에는 hover 색 전환(transition-colors)만 있었습니다. 누른 순간에는 아무 변화가 없다가, 이동이 끝나야 활성 하이라이트가 옮겨갑니다. 응답이 1초 걸리는 환경에서는 이 1초 동안 "눌린 건가?" 하게 됩니다.

링크에 active:scale와 100ms 트랜지션을 넣어서 누르는 순간 살짝 눌리는 느낌을 줬습니다. CSS 한 줄이지만, 눌렀다는 확인이 즉시 오는 것과 안 오는 것의 차이가 큽니다.

응답을 기다리게 만드는 부수 작업

서버 액션의 응답 경로에 부수 작업이 끼어 있었습니다. 얼마 전 감사 로그와 알림 기능을 넣으면서 권한 검사를 통과할 때 로그를 남기고, 상태가 바뀔 때 알림을 적재하게 해뒀는데, 이게 전부 액션 응답을 기다리게 하는 위치에 있었습니다. 버튼 하나 누를 때마다 본 작업에 DB insert가 두 번 더 순차로 붙는 셈입니다.

Next.js의 after()로 옮겨서 응답을 보낸 뒤에 실행되게 했습니다. 문서를 보니 after 콜백은 액션이 에러를 던져도 실행된다고 해서, 권한 거부 기록이 유실될 걱정도 없었습니다. 로깅이나 알림처럼 응답에 꼭 필요하지 않은 쓰기는 처음부터 여기에 두는 게 맞았습니다.

남은 것

이 세 가지를 고쳐도 네트워크 지연 자체는 1초 그대로입니다. 그래도 클릭하는 순간 반응이 오고 스켈레톤이 뜨는 것만으로 "느리다"는 인상은 꽤 줄었습니다. 응답 시간을 줄일 수 없을 때 남는 선택지는 첫 반응까지의 시간을 줄이는 것이고, 그 첫 반응은 대부분 서버가 아니라 CSS와 파일 하나에서 나옵니다.

같은 작업에서 겪은 다른 문제:

Cloudflare를 붙였는데 한국에서 오히려 느려진 이유