본 내용은 2025년 5월 ~ 6월에 진행한 “요이땅” 프로젝트에 대한 트러블슈팅입니다.
“요이땅” 프로젝트를 개발하며 재밌는 문제를 마주하게 되었다.
개인/팀 대시보드 페이지를 서버사이드 렌더링(SSR)을 적용하여 개발을 하였는데, navigation Bar를 통해 페이지를 통해 해당 페이지를 이동하려고 하면 “화면이 멈추는 듯한” 경험을 하게 되었다.
내 PC가 느려서 그런가 했지만, production으로 배포 후 팀원들에게 확인해달라고 했는데도 동일한 증상이 있었다.
“어? 눌렀었나?” 하며 느끼는 불편한 경험이었기 때문에 “사용자 경험”에 있어서 아주 중요한 문제라고 생각하여 개선하게 되었다.
문제가 발생한 페이지 환경
- Next.js 환경에서 SSR으로 렌더링
- 페이지에서 호출하는 백엔드 API 함수는 6개
- 클라이언트 컴포넌트로 동작하는 Naver Map
페이지를 이동하는데 렌더링 되는 시간은 Next 서버의 로그로 쉽게 확인할 수 있었다.
대략 3,000ms ~ 4,000ms 가 걸렸다. 이슈가 발생한 해당 페이지의 “서버 사이드 렌더링(SSR)을 하는 과정에서 딜레이 되는 문제"이다보니 렌더링의 과정을 되집어 보았다.
- Navigation Bar의 페이지 이동 버튼(
<Link>컴포넌트) 클릭한다.- 브라우저는 해당 페이지 경로에 대해 서버에 요청을 보냄
- Next 서버는 해당 경로에 대한
page.tsx컴포넌트를 실행- Next 는
page.tsx를 Server Component로 실행, 내부의 API 호출이 수행됨- API 응답 데이터를 기반으로 해당 페이지 컴포넌트를 렌더링
- 서버에서 전체 페이지의 HTML 문서를 만들어 브라우저 응답을 보냄
- 브라우저는 응답 받은 HTML을 화면에 표시
- 클라이언트 컴포넌트를 Hydration 후 렌더링 완료
페이지 자체가 늦게 로드 되는 문제이다보니 브라우저에 첫 화면에 보여지기까지 즉 FCP 늦게 발생하는 이유를 찾으면 될 것 같았다.

[출처] 한입 크기로 잘라먹는 Next.js 강의
브라우저에 초기 화면이 그려지는 시점(FCP)은 서버로부터 만들어진 HTML을 응답 받았을 때이다. 그렇기 때문에 HTML 자체가 늦게 도착했다고 볼 수 있다.
(이것의 근거로 개발자 도구의 Network 에서 HTML 응답 시간을 확인해보면 된다.)
HTML이 늦게 도착하는 이유에 대해 의심해 볼 수 있는 지점은 당연 “서버에서 컴포넌트를 렌더링하는 시점” 일 것이다.
“Next 서버에서 Server Component가 HTML로 렌더링되는데, 늦어지는 이유는 무엇일까?”
해당 과정에서 느려지는 이유는 간단하게 생각해서 “서버 컴포넌트에서 작업하는 비용이 크기 때문”에 느려지기 때문일 것이다. 다음과 같은 이유를 추측해보았다.
1️⃣ 내부 연산량이 많다.
2️⃣ “API 호출이 지연되기 때문”
3️⃣ 비동기 호출을 순차적으로 실행한다.
4️⃣ Next 서버의 CPU 혹은 메모리의 느린 문제
4번의 경우에는 하나의 배포 서버의 EC2에서 Spring, Jenknis 등 다른 서비스들도 실행되고 있다보니 발생할 수 있는 문제라고 생각했다. 하지만 우리가 받은 EC2의 메모리는 16GB였기 때문에 Next 서버가 돌아가기에 충분했다.
다음은 문제가 발생한 페이지의 컴포넌트이다.
// team/page.tsx
const TeamDashboardPage = () => {
const { nickname, zodiacTeam, sub } = use(getPayloadOrRedirect())
const { data } = use(getMyTeamInfo())
const { zodiacId, ranking, tileCount } = data
const teamInfo = {
username: nickname,
teamName: zodiacTeam,
rank: ranking,
tileCount: tileCount,
zodiacId: zodiacId,
}
return (
<main className="flex flex-1 flex-col gap-10 px-4">
<div className="flex flex-col gap-2">
<TeamTitleSection teamInfo={teamInfo} />
<MyTeamRankCard teamInfo={teamInfo} />
</div>
<TeamContributionSection zodiacId={zodiacId} />
<TeamRankingSummarySection zodiacId={zodiacId} />
<TileMapSectionWrapper type="team" />
<TeamActivityChangeSection />
</main>
)
}
export default TeamDashboardPage
해당 컴포넌트에서 각각의 section 안에서 호출하는 백엔드 API를 모두 합하면 총 7개의 비동기 호출이 있었다.
그래서 1️⃣ ~ 3️⃣ 번의 상태를 확인해보기로 했다.
사실 이 부분은 해당 페이지에서는 고려하지 않아도 된다. 왜냐하면 대부분의 연산들은 백엔드에서 담당 후 넘겨주기로 했기 때문에, 프론트 입장에서는 약간의 가공만 필요할뿐이었다.
<TeamActivityChangeSection/> 컴포넌트안에 각 날짜의 활동 수치를 받아, 주간 활동량의 변화를 계산하는 연산이 있기는 하지만, 시간복잡도 정도의 밖에 없기 때문에 영향이 없을 것이다.
이 부분이 가장 의심이 되는 부분이었다. 우리는 많은 숫자의 유저를 대상으로 운영한다고 생각하고 DB에 많은 더미 데이터를 넣어 놓았다.
문제가 발생한 팀 대시보드 페이지 는 팀에 대한 랭킹 정보, 팀 전체의 런닝 내용, 점령한 타일 등 연관 관계가 복잡하게 얽힌 데이터가 많이 있었다.
나의 입장에서 지금까지는 API 응답 속도를 고려하며 호출하지 않았기 때문에 충분히 확인해볼 여지가 있었다.
정확한 API 응답 속도를 측정하기 위해 즐겨 사용하는 Postman을 사용해주었다.
팀 대시보드 페이지 내부에 있는 6개의 모든 API 응답속도를 5번에 걸처 반복해서 호출하여 측정한 결과, 1개의 API가 다음과 같은 느린 경과를 보여주었다.

캡쳐를 해놓지 않았었는데, 다행히도.. Postman에 History가 남아있었다…!! 만세!!


보통 서비스의 평균 응답 속도가 1초가 넘어가면 사용자 체감 속도가 떨어지고, 2초가 넘어가면 이탈률이 급격히 증가한다고 한다.
"렌더링 지연 문제에 대한 관점"과, "사용자 경험에 대한 관점"으로 보면 중요도가 높은 이슈라는 라는 생각이들어 빠르게 해결하는 것이 필요하다는 판단을 하였다.
처음에는 위에서 분석한 결과 API의 응답 속도를 더해보면 팀 대시보드 페이지 가 로드 되는 시간되는 얼추 비슷해보였다. 그래서 이게 전체적으로 순차적으로 호출이 되나보구나! 생각했었다.
혹시나 하는 마음으로 Cursor AI (Claude-3.7-sonnet Thinking 모델) 로 코드를 분석하여 렌더링 시간을 예측해달라고 부탁했다. 답변결과는 내 예상과 완전 달랐다.

찾아보니 React 18 에서 도입된 동시성 렌더링(concurrent rendering) 으로 내부적으로 작업을 나누어 처리한다고 한다.
동시성 렌더링(concurrent rendering)
여러 작업을 동시에 처리하는 것처럼 보이도록 작업을 쪼개고, 우선순위에 따라 번갈아 실행하는 메커니즘
https://ko.react.dev/blog/2022/03/29/react-v18#what-is-concurrent-react
자세한 내용은 다음에 블로깅하도록 하고, 이 덕분에 각각의 section들이 병렬적으로 렌더링을 한다는 것이다.
(싱글스레드인 자바스크립트에서 어떻게 구현했는지 정말 궁금하다…)
이제와 생각해보니 내가 확인한 팀 대시보드 페이지 로드 시간은 위 내용과 거의 일치했다.
총 로딩 시간 =
getPayloadOrRedirect()+getMyTeamInfo()+Max(각 섹션 API 시간들)
자 이제 문제의 분석을 마쳤으니 해결을 해보자.
가장 첫번째 방법으로 오래 걸리는 API 호출을 바로 담당 팀원에게 공유해주었다. 바로 옆자리에서 개발하고 있었기에 아주 쉽게(?) 소통할 수 있었다. 😆😆😆
“00님, 팀 별 기여도 랭킹을 구하는 API가 느리게 오는 것 같아요 확인 부탁합니다!”
그러고 뚝닥뚝딱… 확인해보시더니 다시 한번 확인해 보라고 하더니 다음과 같이 변해 있었다.. 거의 격변 수준…

어…어떻게 한거에요? 물어보니 원래는 DB에 인덱스를 걸지 않았었는데, 걸어보니 해결이 된 것 같다고 하더라. (내가 미처 알지 못한 부분도 수정이 된 것일 수 있을 것 같다.)
API를 개선하는 것으로 페이지를 넘어갈 때 속도가 아주 충분히 개선이 되었다. 이 정도면 충분하지!! 라는 생각도 했지만, Webview이지만 Native 앱과 같은 부드러운 성능을 원했다. 그래서 사용자 경험 측면에서 개선을 하고 싶은 마음이 있었다.
“왜냐하면… 나는 좋은 사용자 경험을 전하는 향기나는 개발자니까!!!”
그래서 여전히 조금이라도 멈칫하는 경험을 어떻게 개선해줄 수 있을까 고민하다가 Suspense 와 Skeleton UI를 도입하기로 했다. 이 과정에서도 고민이 필요했다.
“어떤 기준과 범위를 정하여 적용을 할 것인가?”
나의 Webview 인사이트를 주었던 “당근 마켓” 을 참고하기로 결정했다. 작년에 진행되었던 2024 당근 테크 밋업 영상 중 아니, 여기도 웹뷰였어요? | 2024 당근 테크 밋업 영상을 아주 인상깊에 보았기 때문이었다!!!
확인해보니 당근은 페이지를 이동할 때 단순히 로딩 바를 보여준다. 반면에 지도 페이지에서는 각각의 요소에 Skeleton UI 를 적용한 모습을 볼 수 있다.
어떤 기준으로 이렇게 적용한 것인지 궁금했다. 아마 페이지를 이동할 때 불러오는 데이터를 각각 로딩 될때마다 보여주기보다 한번에 보여주길 원했던 것이 아닐까 추측이든다.
그래서 나는 다음과 같이 적용하기로 했다.
- 페이지를 이동시에는
로딩 바를 보여주기 → Layout 컴포넌트에서 Suspense 처리- 각각의 요소를 의미있게 보여줄 수 있으며, 무한 스크롤을 적용하는 랭킹 페이지에는 Skeleton UI 처리
먼저 간단하게 Loading UI 를 만들어주었다.
const Loading = () => {
return (
<div className="flex h-[calc(100dvh-150px)] items-center justify-center">
<div className="relative flex h-12 w-12 items-center justify-center">
<span className="border-yoi-500 absolute inline-block h-12 w-12 animate-spin rounded-full border-4 border-r-transparent border-b-transparent border-l-transparent"></span>
</div>
</div>
)
}
그리고 대시보드 페이지 layout.tsx 에 Suspense 를 적용해주었다.
const DashboardLayout = ({ children }: DashboardLayoutProps) => {
return (
<div className="mb-20 flex flex-col gap-4">
<MainHeader />
<Suspense fallback={<Loading />}>{children}</Suspense>
<NavigationBar />
</div>
)
}
결과는 다음과 같다. (이미지가 남아있지 않고, 나의 환경에서 백엔드를 서버를 다시 올리기 시간이 너무 오래걸려 단순한 UI로 대체하였다.)
두번째로 랭킹 페이지에는 Skeleton UI 를 적용해주었다. 보여주는 UI와 같이 하여 최대한 Layout shifting 을 줄여 편안한 경험을 주고 싶었다.
import Skeleton from "@/components/common/skeleton"
interface TeamsContributionSkeletonProps {}
const TeamsContributionSkeleton = ({}: TeamsContributionSkeletonProps) => {
return (
<div className="flex flex-col gap-4">
{Array.from({ length: 10 }).map((_, index) => (
<Skeleton key={index} className="h-16 w-full rounded-xl"></Skeleton>
))}
</div>
)
}
export default TeamsContributionSkeleton

결과적으로 페이지 지연 이슈는 "API 응답속도 지연 되는 현상이 문제"였고, 팀원과 소통하여 백엔드 성능 개선을 통해 해결이 되었다. 그리고 여기서 그치지 않고, Suspense 를 적극적으로 활용하여 로딩 바를 만들고 Skeleton UI 를 적용하여 더욱 자연스러운 사용자 경험 개선을 할 수 있었다.
처음에 속도가 많이 느리다고 했을 때 Next.js 를 처음 적용하는 프로젝트이다보니 갈피를 못잡았었다. 그래서 어떻게 해야할지 막막함도 컸었는데, Next.js의 동작원리에 대해서 다시 깊게 공부하면서 조금씩 원인을 찾아갈 수 있었 던 것 같다.
이번 이슈를 통해서 문제의 원인을 찾아가는 실력은 “기본기” 에 있다는 것을 다시 느끼게 된 것 같다. 또한 React 의 동시성 렌더링과 같은 중요한 원리를 찾게 되고 렌더링 과정을 다시 이해할 수 있게 되어 정말 의미있는 시간이 되었다.
그리고 팀적으로 이 이슈를 공유받았을 때 처음에는 해결의 주체는 나의 몫이라고 생각하고 접근을 했었는데, 팀원과 소통하며 해결해나간 과정속에서 팀 프로젝트는 “함께” 가 되어야 한다는 것을 느낄 수 있었다.
특히 프론트엔드 개발자로 단순히 데이터를 받고, UI/UX만을 신경쓰는 것을 넘어서 백엔드 개발이 내 범위가 아니더라도, 충분히 관심을 가지며 개발을 해야겠다 느낄 수 있었다.
아직 충분히 이해하지 못한 개념들이 많다. 동시성 렌더링으로 파생되는 다양한 개념과 Next를 넘어 SSR의 관점에서 동작하는 원리를 더욱 공부하고 해야겠다.
여러모로 배울것이 많았던 시간이었다.
리액트 18의 신기능 - 동시성 렌더링(Concurrent Rendering), 자동 일괄 처리(Automatic Batching) 등