TMC25 | Engineering - 더 나은 UX를 위한 프론트엔드 전략
https://www.youtube.com/watch?v=Q-_crvz4tv8
해당 내용을 참고해서 글을 작성했습니다.
사용자의 경험을 결정 짓는 최고의 UX
다음 3가지로 정의된다.
- 유려한 UI : 보기 좋고 사용하기 편한 디자인
- 화면의 안정성 : Layout Shift 없는 견고한 화면
- 로딩 속도 : 사용자가 기다리는 시간의 최소화
여기서 로딩 속도를 이용하여 사용자의 UX를 개선하는 계획들을 살펴봤다.
1. SSR(Server Side Rendering)의 전략적 활용이 필요하다.

토스(Toss)의 렌더링 전략: 선택과 집중
화면이 위의 사진과 같다면
- 자산 영역 (상단): 내 통장에 얼마가 있는지 보여주는 가장 중요한 정보. (핵심가치)
- 소비 내역 (하단): 스크롤을 내려야 볼 수 있거나, 상대적으로 덜 급한 정보.
이때 토스는 '자산 영역'을 SSR로 처리하여 서버 통신과 동시에 사용자에게 즉각적으로 보여준다.
반면, 데이터 양이 많고 상대적으로 덜 중요한 '소비 내역'은 클라이언트 사이드에서 비동기로 불러오는 전략을 취한다.
보통 우리는 “서버의 부하를 막기 위해 CSR을 사용한다”거 나 혹은 “SEO(검색 최적화 엔진)을 위해 SSR을 사용한다고 알고 있었다. 하지만 위의 토스의 사례로 “사용자의 시선이 가장 먼저 닿는 곳” 을 먼저 파악하여 해당 부분만을 서버에서 미리 렌더링하여 유저의 경험을 극대화 시켜준다.
SSR 대상 선정 기준
그렇다면 어떤 컴포넌트를 SSR로 태워야 할까?
당장 위에서는 중요한 정보라고 말은 했지만 중요하다고 모든건 SSR로 돌리면 안된다.
SSR은 결국 서버 리소스를 사용하는 비용이 발생하므로 다음 두 가지 측면을 고려해서 선정해야된다.
- 비즈니스 측면: 사용자에게 가장 가치 있는 정보인가? (LCP와 직결)LCP는 사용자가 페이지가 떴을때 인지하는 시점이다. 토스와 같은 핀테크의 가장 큰 목적은 자산이다. 때문에, 가장 핵심 부분이 늦게 뜨면 사용자가 앱 전체가 느리다고 느끼기 때문에 결국 신뢰도 문제로 직결된다.
- 단순히 "중요하다"는 말을 넘어 Core Web Vitals(웹 성능 지표)와 사용자의 심리랑 이어서 생각해야된다.
- 기술적인 측면: 서버 부하를 감당할 수 있는가? 데이터 페칭 비용이 합리적인가?
- SSR은 모든 데이터가 준비될 때까지 응답을 줄 수 없다. 만약 데이터 양이 많고 조회속도가 느린다면 이런경우는 절데 SSR을 담으면 안되다. 이런 경우 뒤의 Caching 전략으로 해결한다.
결국, SSR(Server Side Rendering)의 전략적 활용이 전체적인 UX를 향상시킨다.
2. 체감 속도를 높이는 마법 Caching)
토스는 거래 내역과 같은 데이터를 보여줄 때 로컬 캐싱 전략을 적극적으로 활용한다.

- 최초 진입: API를 호출하여 데이터를 받아오고, 이를 로컬 스토리지에 저장한다.
- 재진입:
- 즉시 렌더링: API 응답을 기다리지 않고, 저장해 둔 이전 내역을 먼저 화면에 뿌려준다.
- 백그라운드 갱신: 뒤단에서 동기 API를 호출하여 최신 데이터를 받아온다
- UI 업데이트: 데이터가 변경되었다면 자연스럽게 화면을 갱신한다.
이 방식을 통해 사용자는 앱을 켤 때마다 "로딩 없이 바로 뜬다"는 쾌적한 경험을 느끼게된다.
3. 자바스크립트 다이어트 (Bundle Diet)
이번 영상을 보며 개인적으로 가장 인상 깊었던, 그리고 미처 깊게 생각하지 못했던 부분이다.
바로 "평가 시간(Evaluation Time)"에 대한 이야기다.
우선 평가 시간이란?
브라우저는 ‘다운로드 → 파싱 → 컴파일 & 실행’ 과 같은 작업을 거치는데 이때 파싱과 컴파일 & 실행 이 평가 시간이다. '평가 시간'이 길어지면 화면은 보이지만 버튼을 눌러도 반응하지 않는(TTI: Time to Interactive가 늦어지는) 현상이 발생한다.
이러한 이유로 평가사간을 줄이기 위해 자바스크립트의 번들 사이즈를 줄여야된다.

다이어트 실천 가이드 (Webpack Bundle Analyzer)
Webpack Bundle Analyzer 같은 도구를 사용하면 프로젝트의 '비만도'를 시각적으로 확인할 수 있다.
분석 결과를 바탕으로 다음 3단계 다이어트를 진행할 수 있다.
1. 미사용 의존성 제거 (Remove Unused Dependencies)
개발을 진행하다 보면 package.json에는 남아있지만, 실제 코드에서는 더 이상 쓰지 않는 '유령 라이브러리'들이 쌓이게 된다. 이를 찾아내는 방법이다.
- 문제점:
- 프로젝트 초기에 테스트용으로 설치했다가 까먹고 지우지 않은 라이브러리.
- 기획 변경으로 기능이 삭제되었으나, 라이브러리 설치 내역은 남은 경우.
- 이들은 번들 사이즈를 키우고 빌드 시간을 늦추게된다.
- 해결 도구: depcheck
- 일일이 코드를 뒤지는 것은 불가능합니다. depcheck라는 CLI 도구를 사용하면 프로젝트를 스캔하여 쓰지 않는 의존성을 찾아준다.
- 설치 및 사용법:
npx depcheck- 실행 결과: Unused dependencies 리스트를 보여준다.
- 주의할 점:
- ESLint, Prettier, Webpack 플러그인 등은 소스 코드 내에서 import 하지 않지만 설정 파일에서 쓰인다. 때문에, depcheck가 이를 '미사용'으로 오탐지할 수 있으니, 무작정 지우지 말고 개발 의존성(devDependencies) 인지 확인 후 삭제해야한다.
2. 경량화 (Lightweight Alternatives)
"기능은 같지만 몸집은 가벼운" 라이브러리를 선택하는 전략이다. 단순히 용량만 줄이는 게 아니라, 트리 쉐이킹(Tree-shaking) 지원 여부가 핵심입니다.
- 대표적인 예시 1: 날짜 라이브러리
- Before (Moment.js): 너무 무겁고, 객체 지향적이며, 내가 쓰지 않는 모든 언어팩(Locale)까지 번들에 포함시킨다.
- After (Day.js / date-fns): Moment.js와 문법이 거의 같으면서 용량은 약 30~100배 가볍습니다. 필요한 기능만 쏙쏙 뽑아 쓸 수 있다.
- 대표적인 예시 2: 유틸리티 라이브러리
- Before (Lodash): import _ from 'lodash' 형태로 통째로 불러오면 번들 사이즈가 급격히 커진다.
- After (Lodash-es): ES Module을 지원하는 lodash-es를 사용하여 필요한 함수만 import { debounce } from 'lodash-es' 형태로 가져오면, 사용하지 않는 나머지 함수들은 번들에서 제외(Tree-shaking)된다.
- 꿀팁 도구: Bundlephobia
- Bundlephobia.com에 라이브러리 이름을 검색하면 용량을 알려주고, 대체 가능한 가벼운 라이브러리(Similar packages)를 추천해 준다.
3. 중복 제거 (Deduplication)
이 부분은 많은 개발자가 놓치는 '숨겨진 살' 같은 존재다. 내가 설치한 라이브러리가 또 다른 라이브러리를 의존하면 발생한다.
발생 원인 (의존성 지옥):
-
- 프로젝트가 A 라이브러리와 B 라이브러리를 사용한다.
- A는 library-x의 1.0 버전을 사용
- B는 library-x의 2.0 버전을 사용
- 결과적으로 내 번들에는 library-x가 두 개(1.0, 2.0) 가 들어가게 되는 문제가 생성된다.
- 확인 방법:
- 터미널에 npm ls <라이브러리명>을 입력하면 중복 설치 여부를 알 수 있다.
- 또는 Webpack Bundle Analyzer를 돌렸을 때 같은 이름의 덩어리가 여러 개 보이면 100% 중복이다.
- 해결 방법:
- npm dedupe (혹은 yarn dedupe): 패키지 매니저가 자동으로 버전을 호환 가능한 범위 내에서 하나로 합치려고 시도한다.
- 강제 통일 (Resolution/Overrides): package.json에 resolutions (yarn) 또는 overrides (npm) 필드를 추가하여 특정 버전을 강제로 쓰도록 명시합니다.JSON
// package.json 예시 "overrides": { "library-x": "2.0.0" }
4. API Waterfall 개선
요청 응답 → 요청 응답 → 요청 응답 → api water fall 개선을 통해 빠르게 처리하는 형식으로 변경한다.
- Waterfall (나쁜 예):
- User 정보 요청 (1초 소요) → 응답 도착 → 게시글 목록 요청 (1초 소요) → 응답 도착
- 총 소요 시간: 2초
- Parallel (좋은 예):
- User 정보 요청 (1초) & 게시글 목록 요청 (1초) → 동시에 시작!
- 총 소요 시간: 1초 (가장 느린 요청 기준)
API Waterfall의 발생원인
① 무의식적인 await의 연속 사용
가장 흔한 실수다. 서로 상관없는 데이터인데 습관적으로 코드를 순서대로 짜는 경우
// ❌ Bad: Waterfall 발생
async function loadData() {
const userData = await fetchUser(); // 이거 끝날 때까지 기다림
const bannerData = await fetchBanner(); // 윗줄 끝나야 시작함
return { userData, bannerData };
}
② 컴포넌트 구조상의 문제 (Fetch-on-Render)
부모 컴포넌트가 다 그려지고(데이터 로딩 완료) 나서야 자식 컴포넌트가 렌더링되는데, 자식 컴포넌트 안에도 API 요청이 있는 경우다.
- [부모] 사용자 프로필 로딩 중... (완료!)
- 렌더링 → [자식] 친구 목록 컴포넌트 등장!
- [자식] 이제 친구 목록 로딩 시작... (또 기다림)
- 렌더링 → [자식] 친구 목록 컴포넌트 등장!
3. 해결 방법 (개선 전략)
① Promise.all로 병렬 처리 (Parallelization)
서로 의존성(인과관계)이 없는 데이터라면 동시에 출발시킨다.
// ✅ Good: 병렬 처리
async function loadData() {
// 두 요청을 동시에 보냄
const [userData, bannerData] = await Promise.all([
fetchUser(),
fetchBanner()
]);
return { userData, bannerData };
}
② 데이터 프리페칭 (Prefetching) / 라우트 레벨 데이터 로딩
컴포넌트가 마운트될 때 요청하는 것이 아니라, 페이지에 진입하는 순간(라우팅 시점)이나 마우스가 버튼에 올라갔을 때 미리 데이터를 요청한다. Next.js의 getServerSideProps나 React Query의 prefetchQuery가 이런 역할을 도와준다.