목록으로

고민 지점 모아보기

약 7분 읽기Development#Development댓글로 이동

개발하면서 고민했던 부분 일단 그냥 모아봐

main
date
Sep 5, 2025
slug
frontend
author
status
Public
tags
Development
summary
개발하면서 고민했던 부분 일단 그냥 모아봐
type
Post
thumbnail
category
Development
updatedAt
Oct 19, 2025 01:48 PM

체크 리스트

nginx 설정 변경
클라우드 플레어 국가 코드 반환 값 header에 추가
기존 라우팅 주소 신규 라우팅 주소로 리다이렉트 되도록 변경
 
클라우드플레어
리소스 캐시 정책 체크
 

프로젝트

  1. 역할 분담
서버사이드, 리액트 쿼리 프리페칭에 대한 이해가 부족했던 것 같다. 랜딩에서 메인 페이지로 넘어갈 때 필요한 데이터를 서버에서 우선적으로 호출하면 좀 더 낫지 않을까 하고 서버 렌더링 시 데이터 페칭을 하도록 하였다.
 
export default async function RankPage(): Promise<ReactElement> { let realtimeChartData: RealTimeChartData[] = []; const queryClient = new QueryClient(); try { const response = await ChartApi.getRealTimeScoreChartData(); const { data } = response; realtimeChartData = data; } catch { realtimeChartData = []; } await Promise.all([ // 실시간 통합 차트 queryClient.prefetchQuery(RankQueries.getRealTimeChartData()), // 댓글 queryClient.prefetchQuery( CommentQueries.getCommentList({ limit: COMMENT_LIMIT, page: 1, order: COMMENT_ORDER.DESC, }), ), ]); const dehydratedState = dehydrate(queryClient); // 이하 생략
 
위 코드로 랜딩에서 메인으로 넘어가서 페인팅 되는데까지 대략 1.7s ~ 3s가 걸리게 되었다.
아니 프로덕션 환경인데 저러면 문제가 있다고 판단…
 
데이터를 페칭한 부분은
  1. 라인 차트를 그리기 위한 3일치의 노래 순위 데이터
  1. 리액트 쿼리 프리페칭
      • 실시간 차트 데이터 프리 페칭
      • 댓글 1 페이지 프리 페칭
 
우선 프리 페칭을 한 데이터의 경우 의미가 없다고 판단하여 삭제
→ 댓글의 경우 최 하단에 존재한다. 그렇기 때문에 사용자가 메인 페이지에 접근했을 때 스크롤을 제일 아래로 내려야만 볼 수 있는 화면이라 데이터를 불러와서 캐싱해두는것이 의미없는 것 같았다.
 
실시간 차트 프리페칭도 삭제
→ 메인 페이지로 넘어가면 상단에 뉴스, 팬캘린더, 실시간 차트(라인 차트) 세 컨텐츠가 보여진다. 그렇기 때문에 프리페칭한 데이터 영역은 사용자가 어느정도 스크롤을 해야만 볼 수 있는 영역
 
export default async function RankPage(): Promise<ReactElement> { const chartApiResponse = await ChartApi.getRealTimeScoreChartData().catch(() => ({ data: [] })); const realtimeChartData: RealTimeChartData[] = chartApiResponse.data ?? []; // 이하 생략
 
우선 이렇게만 해도 페인팅까지 0.4s ~ 1.2s 까지 줄였다.
 
  1. 초기 렌더링 시 흰 화면 지양
흰 화면을 보여주지 않는건 너무나도 당연한 것 같다. 사용자 입장에서 흰 화면이면 내가 뭐 잘못 들어왔나??? 싶을 것 이다. React의 Suspense를 활용하여 부분적으로 지연 로딩을 최대한 활용 해봐야겠다.
 
공용 레이아웃을 사용하고 있는데 새로고침을 해도 헤더와 푸터 마저도 보이지 않는 그냥 흰 화면이 노출되었다가 컨텐츠 페칭이 끝나면 화면이 보여진다. 상당히 잘못됐다
 
헤더와 푸터는 공용 layout.tsx 파일에 분리해놓은 상태고 메인 컨텐츠는 children으로 받는다. 근데 children이 늦어진다고 그 늦어진 기간만큼 화면에 아무것도 안그려진다는건 사용자 입장에서 UX를 저하 시키는 원인이 될 것 같다.
 
children을 suspense로 감싸서 페인팅 전까지 로딩을 보여준다.
 
notion image
 
그러면 사용자 입장에서 컨텐츠가 보여질 때까지 1초 정도는 뭐 더 기다려주지않을까?? 아무것도 없는 흰 화면 보다 나으니까. 여기서 추가적으로 고민은 라우트 프로그레스를 넣을까 말까
→ 내부 이동의 경우 next link를 사용하는데 이 때 클릭 후 화면에 페인팅까지 약간의 딜레이가 있는 것 같다. 해당 페이지에 병목 현상을 제거 해야하는건 너무나 당연한거고 사용자 입장에서 클릭 했을 때 일단 서버가 움직이고 있으니 잠시만 기다려달라라는 의미로 뭘 보여주는게 오픈이 얼마 안남은 이 시점에서 가장 빠르게 처리할 수 있는거 아닐까 싶은데
 
심지어 아이차트의 경우 사용자 풀이 외국인들이 많으니까 더 느릴 것 으로 예상된다.
 
  1. 캐시 정책
정적 리소스에 대한 캐시 정책을 정의하려고 한다. 매 번 정적 리소스를 내려받는건 불필요한 행위로 보여진다.
 
우선 nextconfig에 정적 리소스에 대한 캐시 처리를 해보자.
 
import bundleAnalyzer from '@next/bundle-analyzer'; import type { NextConfig } from 'next'; const isAnalyzeMode = process.env.ANALYZE === 'true'; const nextConfig: NextConfig = { output: 'standalone', reactStrictMode: true, experimental: { scrollRestoration: true, forceSwcTransforms: true, // SWC transform 강제 esmExternals: true, // ESM 모듈 최적화 optimizePackageImports: [ 'date-fns', 'usehooks-ts', '@instiz/core', '@instiz/uikit', 'tailwind-variants', ], }, productionBrowserSourceMaps: false, // 브라우저용 sourcemap 제거 pageExtensions: ['tsx', 'ts', 'jsx', 'js'], images: { formats: ['image/avif', 'image/webp'], minimumCacheTTL: 31536000, // 1년 deviceSizes: [360, 414, 768, 1024, 1280], imageSizes: [16, 24, 32, 48, 64, 96, 128, 256, 384, 512], remotePatterns: [ { protocol: 'https', hostname: 'www.instiz.net', port: '', pathname: '/**', }, { protocol: 'https', hostname: 'file3.instiz.net', port: '', pathname: '/**', }, { protocol: 'http', hostname: 'file3.instiz.net', port: '', pathname: '/**', }, { protocol: 'https', hostname: 'scrap.kakaocdn.net', port: '', pathname: '/**', }, { protocol: 'http', hostname: 'scrap.kakaocdn.net', port: '', pathname: '/**', }, { protocol: 'https', hostname: 'pbs.twimg.com', port: '', pathname: '/**', }, ], }, typescript: { ignoreBuildErrors: true, }, eslint: { ignoreDuringBuilds: true, }, compiler: { removeConsole: { exclude: ['error', 'warn'] }, reactRemoveProperties: true, }, async headers() { const longCache = { key: 'Cache-Control', value: 'public, max-age=31536000, immutable', }; const shortCache = { key: 'Cache-Control', value: 'public, max-age=60', }; return [ { source: '/_next/static/:path*', headers: [longCache], }, { source: '/_next/image', headers: [longCache], }, { source: '/:path*', headers: [shortCache], }, ]; }, webpack: (config, { isServer, dev, webpack }) => { config.module.rules.push({ test: /\.tsx?$/, include: [/packages\/core/, /packages\/uikit/], use: [ { loader: 'ts-loader', options: { transpileOnly: true, }, }, ], }); if (!dev) { config.optimization = { ...config.optimization, moduleIds: 'deterministic', chunkIds: 'deterministic', runtimeChunk: 'single', splitChunks: { chunks: 'all', minSize: 20000, maxSize: 150000, automaticNameDelimiter: '-', cacheGroups: { reactVendor: { test: /[\\/]node_modules[\\/](react|react-dom)[\\/]/, name: 'react-vendor', chunks: 'all', priority: 20, }, // Swiper 등 대형 UI는 따로 uiVendors: { test: /[\\/]node_modules[\\/](swiper|echarts|@radix-ui)[\\/]/, name: 'ui-vendors', chunks: 'all', priority: 10, }, defaultVendors: { test: /[\\/]node_modules[\\/]/, priority: -10, reuseExistingChunk: true, }, }, }, minimize: true, }; } if (!isServer) { config.optimization = { ...config.optimization, splitChunks: { chunks: 'all', minSize: 20000, maxSize: 150000, automaticNameDelimiter: '-', cacheGroups: { reactVendor: { test: /[\\/]node_modules[\\/](react|react-dom)[\\/]/, name: 'react-vendor', chunks: 'all', priority: 20, }, defaultVendors: { test: /[\\/]node_modules[\\/]/, priority: -10, reuseExistingChunk: true, }, }, }, minimize: true, }; } return config; }, }; const withBundleAnalyzer = bundleAnalyzer({ enabled: isAnalyzeMode, }); export default withBundleAnalyzer(nextConfig);
 
근데 개발 서버에서 확인해본 결과 매 번 새로운 리소를 내려 받고 있다. 즉 저 설정이 의미가 없다는 것
 
우리 서비스는 클라우드 플레어를 거쳐서 서빙된다. 그렇기 때문에 클라우드 플레어 캐시 정책이 nextconfig를 씹어 먹는다. 현재 개발 도메인의 경우 아무런 캐시 정책을 해두지 않은 상태이다. 이 부분도 고령하여 서비스 배포 시 체크 필요

사소한 것

  1. 번들 사이즈
monorepo 기반 nextjs 프로젝트 도커 빌드 시 1.08gb (1.08gb가 많다면 많고 적다면 적은데 페이지가 뭐 10장 이하인데 이러면 문제있다고 판단됨..)
→ @next/bundle-analyzer 을 활용해서 번들 사이즈 체크 후 정리 시작
  • 코어 라이브러리 관리
pnpm 을 활용하고 있기 때문에 각 프로젝트별로 중복된 npm을 설치해도 node_modules가 커지지 않음
그렇지만 각 패키지별로 버전 관리가 되지 않는다. 어차피 모노레포를 구축하면서 코어를 담당할 패키지 또한 셋업을 해두었고 clsx의 경우 이미 코어 패키지를 통하여 세 프로젝트에서 사용중이다.
 
각 프로젝트 별로 코어에서 관리할 수 있는 것들은 이동하여 통합 관리한다.
 
  1. 개발 서버와 운영 서버 분리 (SEO)
지금 개발 서버 배포 상태가 운영과 동일하다. (아무런 처리가 안되어있었다..ㅎㅎ) 그래서 개발 서버를 구글에 검색하면 잡힌다.
 
개발 서버일 때 사용하는 meta와 운영 서버의 meta를 분리할 필요가 있어보인다. 그렇다면 개발 서버인 것을 배포할 때 어떻게 알려야할까
next build 환경의 경우 기본적으로 production으로 간주하여 처리한다고 했는데
 
같은 운영 환경이라도 staging과 production으로 나눠서 메타 데이터를 관리할 수 있는 방향을 찾아야한다.
 
  1. 패키지 모듈 버전 관리
현재 패키지를 설치할 때의 버전이 로컬 기반으로 관리되도록 되어있다. 그래서 패키지가 업데이트 되면 4개의 서비스의 재 배포가 일어나는 상황이다. (항상 최신 패키지를 사용하도록 하려면 이제 맞는 플로우 같으면서도 버전을 명시하면 이럴 필요 없지 않나 생각이 든다.)
 
예전에 github의 package를 통해 공용 모듈을 배포해본 경험이 있었다. 그래서 gitlab에도 동일한 기능이 있나 확인해보니 package registry를 제공해주는 것을 확인했고 테스트 퍼블리싱까지 해둔 상태이다.
 
notion image
 
이 버전을 도입했을 경우 이제 모든 서비스에서는 다운 받을 패키지 버전을 명시하도록 변경해야한다.