프론트엔드의 중심은 오랫동안 “어떻게 화면을 잘 그릴 것인가”에 있었다.
컴포넌트를 잘 나누고, 상태를 잘 관리하고, DOM 업데이트를 효율적으로 만드는 일이 핵심이었다.
지금도 물론 중요하다. 그런데 최근 몇 년 사이, 좋은 프론트엔드를 가르는 기준은 조금 달라졌다.
이제 더 자주 하게 되는 질문은 이런 것들이다.
이 화면에서 사용자가 제일 먼저 봐야 하는 건 무엇인가. 그건 언제 도착해야 하는가. 무엇은 지금 당장 준비해야 하고, 무엇은 조금 늦어도 되는가. 어떤 비용은 먼저 내고, 어떤 비용은 뒤로 미뤄야 하는가.
그래서 요즘 프론트엔드는 렌더링 자체보다 “준비와 공개의 순서를 설계하는 일”에 더 가까워졌다고 느낀다.
예를 들어, preload, prefetch, SSR, 스트리밍 렌더링, Suspense, loader는 서로 다른 기술처럼 보이지만, 결국 같은 문제를 다루고 있다.
현대 프론트엔드는 UI를 그리는 기술이기도 하지만, 그보다 먼저 리소스와 데이터와 CPU와 사용자 경험의 우선순위를 정하는 기술이다.
왜 더 중요해졌나
예전에는 파일 크기가 제일 큰 적이었다. 번들 크기를 줄이고, 이미지를 압축하고, 캐시만 잘 잡아도 체감이 꽤 좋아졌다. 물론 빌드 및 배포 설정을 최적화해서 해시 재사용이 잘 되게 하거나, 벤더 청크를 더 잘게 나누는 일도 여전히 무척 중요하다.
다만 실제 서비스에서 성능이 무너지는 지점을 보면 문제가 조금 더 다이나믹하다고 해야할까, 넓은 시야에서 바라봐야 하는 것들이 생긴다.
요즘 병목은 보통 이런 식으로 발견되기도 한다.
브라우저가 중요한 리소스를 늦게 발견한다. JS가 내려오기 전까지 데이터 요청이 시작되지 않는다. 페이지 전체가 준비될 때까지 아무것도 못 보여준다. 화면은 보이는데 하이드레이션이나 무거운 렌더링 때문에 클릭이 잘 안 먹는다. 다음 화면으로 갈 가능성이 높은데도 아무 준비를 하지 않는다.
즉 다운로드 속도만의 문제가 아니다.
언제 발견되는가.
언제 요청되는가.
언제 보여지는가.
언제 반응하는가.
요즘 체감 성능은 이 순서에서 갈린다.
사용자 기대치도 달라졌다. 이제는 웹에서도 앱 같은 반응성을 기대한다.
조금 빨리 뜨는 것만으로는 부족하다. 눌렀을 때 바로 반응해야 하고, 이동할 때 버벅이지 않아야 하고, 일부 데이터가 늦더라도 전체 경험이 끊기지 않아야 한다.
그래서 프론트엔드는 점점 “전부 준비되면 한 번에 보여주는 방식”에서 멀어지고 있다.
중요한 것을 먼저 보여주고, 덜 중요한 것은 나중에 채우고, 사용자 의도가 보이면 미리 준비하는 쪽으로 계속 이동하고 있다.
이 개념들을 이해하려면 화면을 컴포넌트 트리로만 보면 잘 안 보인다. 시간축으로 봐야 한다.
리소스
브라우저는 HTML을 읽으면서 CSS, JS, 폰트, 이미지 같은 리소스를 발견하고 요청한다.
문제는 중요한 리소스가 HTML에 바로 드러나지 않으면 브라우저도 그것을 늦게 발견한다는 점이다.
JS를 실행한 뒤에야 알 수 있는 이미지, 늦게 import되는 코드, 뒤늦게 드러나는 스타일은 그만큼 늦어진다.
여기서 preload와 prefetch가 등장한다.
preload는 “이건 현재 화면에서 곧 반드시 필요하니 지금 당겨라”에 가깝다.
prefetch는 “이건 다음에 필요할 가능성이 있으니 한가할 때 미리 가져와라”에 가깝다.
둘은 비슷해 보여도 역할이 다르다.
preload는 현재 경험을 위한 것이고, prefetch는 다음 경험을 위한 것이다.
그래서 중요한 건 API 이름보다 “무엇을 현재의 핵심 리소스로 볼 것인가”를 나눌 수 있는 감각이다.
화면 공개
사용자는 화면이 언제 “완성”됐는지보다 언제 “의미 있는 것”을 볼 수 있었는지를 더 민감하게 느낀다.
SSR이 중요한 이유가 여기에 있다. 서버가 초기 HTML에 실제 콘텐츠를 실어 보내면, 브라우저는 더 빨리 구조와 내용을 보여줄 수 있다.
반대로 모든 것을 클라이언트 JS가 받은 뒤에 그리게 하면, 빈 화면이나 스켈레톤만 길게 보게 되는 경우가 있다.
여기서 한 단계 더 나아간 게 스트리밍 렌더링이다.
예전 방식이 페이지 전체가 다 준비될 때까지 기다렸다가 한 번에 보내는 쪽이었다면, 스트리밍은 먼저 준비된 셸을 보내고 늦는 데이터나 무거운 영역은 뒤에서 흘려보낸다.
이 사고의 핵심은 단순하다.
페이지는 이제 하나의 완성본이라기보다, 순서대로 공개되는 조각들의 묶음에 가깝다.
데이터
현대 프론트엔드에서는 데이터를 어디서 가져오느냐보다 언제 가져오기 시작하느냐가 더 중요할 때가 많다.
보통의 FE 개발에서는 페이지 이동 후 컴포넌트가 마운트되고, 그 뒤 effect에서 fetch를 시작한다.
사용자는 이미 이동을 눌렀고, 브라우저는 JS를 받았고, 컴포넌트는 그려졌는데, 데이터는 그제야 출발한다.
이 구조는 거의 항상 waterfall을 만든다.
예를 들면 loader나 Suspense가 중요한 이유는 여기 있다.
loader의 본질은 단순한 fetch 헬퍼가 아니다. 데이터 준비를 렌더 이후가 아니라 렌더 이전 혹은 내비게이션 단계로 끌어올리는 장치에 가깝다.
핵심은 데이터 의존성을 어디에 배치하느냐다.
상호작용
이 부분은 자주 깜빡할 수 있는 영역일 수 있다.
화면이 빨리 보여도 바로 반응하지 않으면 사용자는 느리다고 느낀다.
클릭했는데 눌린 느낌이 늦게 오고, 입력창이 버벅이고, 탭 전환 시 CPU가 과하게 점유되면 네트워크와 관계없이 앱이 답답하게 느껴진다.
그래서 중요한 건 “얼마나 빨리 데이터를 가져오느냐”만이 아니다.
어떤 업데이트가 급한지, 어떤 업데이트는 나중이어도 되는지, 어떤 작업은 쪼개야 하는지 아는 것이 중요하다.
Suspense, transition, selective hydration 같은 개념도 결국 이 상호작용 타임라인과 맞닿아 있다.
preload와 prefetch
preload와 prefetch를 “속도 올리는 옵션”으로만 보는 경우가 있지만, 나는 이 둘을 브라우저에게 미래의 중요한 데이터를 조금 더 빨리 알게 해주는 힌트라고 생각한다.
현재 화면의 핵심 리소스라면 preload가 맞다. 다음 화면으로 갈 확률이 높다면 prefetch가 맞다.
당연히 모든 링크를 다 prefetch하는 건 좋지 않다. 지금 당장 필요 없는 걸 preload하는 것도 마찬가지다.
중요한 건 기술을 아는 것보다 리소스의 우선순위를 분류할 줄 아는 것이다.
지금 반드시 필요하다. 곧 필요할 가능성이 높다. 있으면 좋지만 없어도 된다. 백그라운드에서 천천히 가져와도 된다.
이 분류가 있어야 preload와 prefetch를 제대로 사용할 수 있다.
SSR과 스트리밍
프론트엔드 커뮤니티에서는 오랫동안 CSR이냐 SSR이냐 같은 식으로 이야기가 많이 흘렀다.
그런데 사실 이 구분 자체보다 “이 화면에서 어떤 병목을 풀어야 하는가”가 더 중요해진 느낌이다.
예를 들어, 마케팅 페이지는 SEO와 초기 콘텐츠 노출이 중요하니 SSR이 강하다. 상품 상세는 핵심 영역은 SSR로 보내고, 추천 영역은 나중에 채워도 된다. 대시보드는 초기 셸만 빠르게 보여주고, 데이터 패널은 스트리밍이나 Suspense 경계로 나눌 수 있다. 에디터나 데이터 그리드처럼 상호작용이 더 중요한 화면은 SSR보다 메인 스레드 관리가 더 중요할 수도 있다.
결국 중요한 건 CSR이냐 SSR이냐가 아니다.
이 화면의 병목이 무엇이고, 어떤 조합이 가장 자연스러운가가 더 중요하다.
Suspense
Suspense를 단순히 fallback을 보여주는 API로 이해하면 절반만 이해한 셈이라고 생각한다.
Suspense의 핵심은 비동기를 UI 구조 안으로 끌어와서 “어디까지는 먼저 보여주고, 어디부터는 기다릴 것인가”를 경계로 표현하게 만든다는 데 있다.
즉 Suspense는 로딩 UI API라기보다 공개 순서와 실패 범위를 설계하는 도구에 가깝다.
좋은 경계는 대개 사용자 경험 경계와 닿아 있다.
상단 헤더와 기본 레이아웃은 바로 보여야 한다. 핵심 본문은 가능한 빨리 보여야 한다. 추천 목록이나 사이드 패널은 조금 늦어져도 된다. 일부 위젯이 실패해도 전체 페이지는 유지되어야 한다.
이렇게 경계를 잘 나누면 느린 부분이 전체를 붙잡지 못한다.
반대로 Suspense를 너무 잘게 쪼개거나, 의미 없는 단위로 나누면 오히려 화면이 산만해진다.
loader
loader도 종종 오해된다. 컴포넌트 바깥에 fetch 함수를 빼는 패턴 정도로 받아들이기 쉽다.
그런데 본질은 코드의 분리가 아니다.
loader는 데이터를 렌더 이후가 아니라 라우팅과 렌더 파이프라인의 앞단에서 다루게 만든다. 즉 데이터 의존성의 위치를 바꾸는 것이다.
컴포넌트 마운트 뒤에 fetch를 시작하면,
렌더 -> effect -> fetch -> 응답 -> 재렌더
이 순서가 된다.
반대로 내비게이션 단계에서 미리 준비하면,
이동 시작 -> 데이터 준비 -> 공개 혹은 스트리밍
같은 흐름을 만들 수 있다.
이 구조 차이가 waterfall을 줄이고, 취소를 쉽게 만들고, 재검증 정책을 통제하게 해준다.
상품 상세 페이지를 생각해보기
이 개념들을 실제로 어디에 써야 하는지가 더 중요하다. 상품 상세 페이지 하나를 놓고 보면 꽤 분명해진다.
사용자가 제일 먼저 봐야 하는 건 보통 이쪽이다.
- 상품명
- 가격
- 대표 이미지
- 옵션 선택 영역
- 구매 버튼
이 다섯 가지는 페이지의 핵심 경험에 가깝다. 그래서 여기에는 같은 기준이 적용되기 어렵고, 우선순위를 높게 잡는 편이 맞다.
예를 들어 대표 이미지가 위쪽에서 크게 보이는 LCP 이미지라면 이건 preload 후보가 된다.
핵심 상품 정보도 페이지 진입 뒤 effect에서 기다릴 게 아니라 내비게이션 단계에서 loader로 먼저 당겨오는 쪽이 낫다.
SSR로는 상품명, 가격, 대표 이미지, 기본 옵션 상태, 구매 버튼 정도까지는 바로 보이게 보내는 편이 자연스럽다.
이렇게 하면 사용자는 페이지에 들어오자마자 “내가 보려던 상품이 맞다”는 확신을 빨리 얻는다.
반대로 아래 영역은 조금 늦어도 된다.
- 리뷰 목록
- 추천 상품
- 최근 본 상품
- 판매자 다른 상품
- 개인화 위젯
이런 영역은 대개 Suspense 경계로 분리하기 좋다. 핵심 구매 흐름을 막지 않기 때문이다.
스트리밍 렌더링을 쓴다면 첫 응답에는 상단 셸과 상품 요약만 먼저 보내고, 리뷰나 추천 상품은 뒤에서 흘려보내는 구성이 잘 맞는다.
그러면 느린 리뷰 API 하나 때문에 상단 구매 영역 전체가 같이 묶여서 늦어지는 일을 줄일 수 있다.
다음 화면 준비도 의도에 따라 달라진다.
상품 목록에서 상세로 들어갈 가능성이 높다면, 리스트에서 링크 hover나 viewport 진입 시점에 상세 페이지 청크나 핵심 데이터를 prefetch할 수 있다.
다만 모든 상품 링크를 한꺼번에 다 당기는 식은 대개 과하다. 사용자 의도가 보이는 몇 개만 고르는 편이 낫다.
상호작용 쪽도 같이 봐야 한다.
옵션 선택, 수량 변경, 구매 버튼 반응은 급하다. 이건 바로 반응해야 한다.
반면 리뷰 정렬 변경이나 하단 추천 캐러셀 갱신처럼 당장 구매를 막지 않는 업데이트는 transition으로 넘겨도 된다.
이렇게 나누면 사용자는 페이지가 빨라졌다고 느낀다기보다, 답답하지 않다고 느끼게 된다.
세심한 차이를 느낄 수 있는 개발자라면, 생각보다는 이 차이가 꽤 클 수 있다.
결국은 스케줄링
위에서 설명한 대부분의 개념들은 따로 동작하는 개념이 아니다.
preload와 prefetch는 리소스 요청 순서를 다룬다. SSR과 스트리밍 렌더링은 HTML 공개 순서를 다룬다. Suspense는 화면 공개 경계를 다룬다. loader는 데이터 준비 시점을 다룬다. transition과 동시성 제어는 CPU와 상호작용 우선순위를 다룬다.
결국 같은 질문으로 모인다.
무엇을 먼저 준비할 것인가. 무엇을 먼저 보여줄 것인가. 무엇을 먼저 반응하게 만들 것인가.
현대 프론트엔드를 이해한다는 건 결국 이 세 질문에 답할 수 있다는 뜻에 가깝다.
그래서 무엇을 할 수 있어야 하나
프레임워크는 계속 바뀌고, API는 계속 바뀐다.
1. 브라우저 시점으로 볼 수 있어야 한다
핵심 리소스는 언제 발견되는가. HTML에 바로 드러나는가. JS 실행 뒤에야 드러나는가. 어떤 CSS와 JS가 초기 렌더를 막는가. 하이드레이션은 언제 시작되는가.
이 질문에 답할 수 있어야 한다.
2. 우선순위를 나눌 수 있어야 한다
지금 꼭 필요한 것, 곧 필요할 것, 나중에 필요할 것을 구분할 수 있어야 한다.
오히려 모든 것을 한 번에 잘하려 들면 대체로 실패한다.
3. 경계를 설계할 수 있어야 한다
컴포넌트 경계만이 아니라, 로딩 경계, 실패 경계, 재검증 경계, 공개 경계를 설계할 수 있어야 한다.
어디서 fallback을 보여줄지, 어디서 실패를 흡수할지, 어디까지는 반드시 같이 보여야 하는지, 이 판단을 할 수 있는 감각을 가지는게 중요하다.
4. 데이터 흐름을 언제의 관점에서 볼 수 있어야 한다
데이터를 어디서 부르느냐보다, 언제부터 흐르게 만들 것이냐가 더 중요할 때가 많다.
fetch-on-render인지, render-as-you-fetch인지, 내비게이션 단계에서 미리 준비하는지, 이 차이를 볼 수 있어야 한다.
5. CPU를 예산처럼 다룰 수 있어야 한다
네트워크만 최적화한다고 끝나지 않는다. 무거운 리스트 렌더링, 비싼 계산, 과도한 상태 전파, 대규모 하이드레이션은 상호작용을 쉽게 망친다.
쉬운 개념은 아닐 수 있지만, 이제는 메인 스레드도 하나의 제한된 예산처럼 봐야 한다.
6. 측정하고 검증할 수 있어야 한다
결국 제일 중요한 건 측정이다. “왠지 빨라진 것 같다”는 보통 오래 못 간다.
실제 병목이 리소스 발견 지연인지, 데이터 waterfall인지, CPU 점유인지, 하이드레이션인지 구분할 수 있어야 한다.
좋은 개발자는 최적화 기법을 많이 아는 사람이기보다, 지금 앱의 병목이 무엇인지 설명할 수 있는 사람에 더 가깝다.
어떻게 공부할까
이 주제를 체화하려면 프레임워크 튜토리얼만 반복해서는 조금 부족할 수 있다.
먼저 브라우저 네트워크와 렌더링 파이프라인을 보는 게 좋다. 리소스가 어떻게 발견되고, 무엇이 렌더를 막고, 메인 스레드가 언제 바빠지는지 이해해야 한다.
그다음에는 화면 하나를 잡고 직접 타임라인으로 분석해보면 좋다.
예를 들어 상품 상세 페이지를 놓고 이렇게 물어보는 식이다.
어떤 리소스가 가장 먼저 필요했는가. 어떤 데이터는 페이지 이동 전에 준비할 수 있었는가. 어디를 먼저 보여주고, 어디는 나중에 보여줄 수 있었는가. 어떤 부분이 실제 상호작용 지연을 만들었는가.
이런 식으로 한 번 분해해보고 나서 React, Router, Suspense, SSR, 스트리밍, Query Library, Build Tool을 다시 보면 좀 더 명확하게 청사진이 보이기 시작할 것이다.
마지막으로
좋은 프론트엔드 개발자는 컴포넌트를 예쁘게 만드는 사람만은 아니다. 상태 관리를 잘하는 사람만도 아니다.
결국 이런 질문을 풀어내는 사람에 더 가깝다.
무엇이 가장 먼저 보여야 하는가. 무엇이 가장 먼저 반응해야 하는가. 무엇을 미리 준비해야 하는가. 무엇은 늦어져도 되는가. 어디서 실패를 격리해야 하는가. 어떤 비용은 지금 내고, 어떤 비용은 나중에 내야 하는가.
예를 들어, preload, prefetch, SSR, 스트리밍 렌더링, Suspense, loader는 이 질문들에 답하기 위한 서로 다른 수단이다.
중요한 건 특정 기술을 쓸 줄 아는 것보다, 언제 어떤 수단을 써야 하는지 아는 것이다.
그래서 요즘 프론트엔드는 단순히 UI를 그리는 기술이라기보다, 사용자 경험의 시간 구조를 설계하는 기술에 더 가까워졌다고 느낀다.