1. 문제 발생❓
params를 이용한 동적 렌더링 페이지 4개(DynamicPage 컴포넌트)를 구현했습니다. params의 pageId값을 받아 pageId값과 일치하는 데이터를 찾아 각각의 pageConfig 데이터를 초기에는 클라이언트 측에서 렌더링 하는 방식으로 구현했습니다. 하지만 전체 페이지가 클라이언트 컴포넌트로 설정되어 있어 이로 인해 초기 페이지 로딩 시 이미지 렌더링 속도가 현저히 느려지는 문제가 발생했습니다.
2. 원인 분석 🔎
Next.js에서 클라이언트 컴포넌트 "use client"로 설정된 컴포넌트는 서버에서 사전 렌더링 되지 않고 클라이언트 측에서 hydration이 완료된 후에만 완전히 렌더링됩니다. 전체 DynamicPage 컴포넌트가 클라이언트 컴포넌트였기 때문에:
- 모든 이미지 렌더링이 클라이언트 측에서 이루어져야 했음
- 자바스크립트 번들이 다운로드되고 실행될 때까지 이미지 요청이 지연됨
- 결과적으로 초기 페이지 로드 시 이미지가 늦게 표시됨
< 개선 전 코드 >
"use client"; // 전체 페이지가 클라이언트 컴포넌트로 설정
import styles from "../../css/background.module.css";
import Image from "next/image";
import PrivacyPolicyText from "@/components/PrivacyPolicyText";
import { PageConfig } from "@/mocks/resourcesData";
import RequestMaterialForm from "../../components/RequestMaterialForm";
import Text from "@/app/components/typography/Text";
import { useIsMobile } from "@/app/hooks/useIsMobile";
export default function DynamicPage({ config }: { config: PageConfig | null }) {
if (!config) return;
const isMobile = useIsMobile(); // 이 훅 때문에 전체 컴포넌트가 클라이언트 컴포넌트가 되어야 했음
return (
<main className="flex flex-col w-full">
{/* Hero */}
<div
className={`flex justify-center w-full mt-[72px] ${config.hero.bgColor} mobile:mt-[50px]`}
>
{/* 모바일 여부에 따라 다른 이미지를 직접 조건부 렌더링 */}
{isMobile ? (
<Image src={config.hero.mobile} alt={config.id} width={768} height={721.06} />
) : (
<Image
src={config.hero.desktop}
alt={config.id}
width={1440}
height={451}
className="min-w-[1440px] h-[451px]"
/>
)}
</div>
{/* 나머지 컴포넌트들... */}
</main>
);
}
3. 해결 과정 📋
문제 해결을 위해 다음과 같은 접근 방식을 진행했습니다:
- useIsMobile 훅을 사용하는 부분만 별도의 클라이언트 컴포넌트(HeroElement)로 분리
- 메인 Dynamic 컴포넌트는 서버 컴포넌트로 변경하여 초기 렌더링 성능 개선
- 분리된 클라이언트 컴포넌트는 필요한 부분에만 적용
< 개선 후 코드 >
1. 메인 DynamicPage 컴포넌트 (서버 컴포넌트로 변경)
// "use client" 제거 - 서버 컴포넌트로 변경
import styles from "../../css/background.module.css";
import Image from "next/image";
import PrivacyPolicyText from "@/components/PrivacyPolicyText";
import { PageConfig } from "@/mocks/resourcesData";
import RequestMaterialForm from "../../components/RequestMaterialForm";
import Text from "@/app/components/typography/Text";
import Description1Element from "./Description1Element";
import Description2Element from "./Description2Element";
import HeroElement from "./HeroElement"; // 분리된 클라이언트 컴포넌트 import
export default function DynamicPage({ config }: { config: PageConfig | null }) {
if (!config) return;
return (
<main className="flex flex-col w-full">
{/* Hero */}
<div
className={`flex justify-center w-full mt-[72px] ${config.hero.bgColor} mobile:mt-[50px]`}
>
<HeroElement
id={config.id}
mobile={config.hero.mobile}
desktop={config.hero.desktop}
/>
</div>
{/* 나머지 컴포넌트들... */}
</main>
);
}
2. 분리된 HeroElement 컴포넌트 (클라이언트 컴포넌트)
"use client"; // 클라이언트 컴포넌트로 설정
import { useIsMobile } from "@/app/hooks/useIsMobile";
import Image from "next/image";
const HeroElement = ({
id,
mobile,
desktop,
}: {
id: string;
mobile: string;
desktop: string;
}) => {
const isMobile = useIsMobile(); // 이 훅은 클라이언트에서만 작동
return (
<>
{isMobile ? (
<Image src={mobile} alt={id} width={768} height={721.06} />
) : (
<Image
src={desktop}
alt={id}
width={1440}
height={451}
className="min-w-[1440px] h-[451px]"
/>
)}
</>
);
};
export default HeroElement;
3. 개선 효과
- 초기 로딩 속도 향상 ⬆️
- Hydration 최소화 ⬇️
- 이미지 최적화 유지 ✅
4. 결론 ❤🔥
이 사례는 Next.js 애플리케이션 컴포넌트 구조를 적절히 분리하는 것이 성능에 얼마나 큰 영향을 미칠 수 있는지 보여줍니다! 클라이언트 사이드 로직이 필요한 부분만 정확히 식별하고 분리함으로써, 서버 컴포넌트의 장점을 최대한 활용하고 클라이언트 사이드 렌더링의 오버헤드를 최소화할 수 있었습니다.
'인턴' 카테고리의 다른 글
React Hook Form과 Zod를 활용한 효율적인 폼 유효성 검사 (0) | 2025.04.03 |
---|---|
[ Troubleshooting🛠️ ] Next.js에서 Tailwind CSS 설치 시 발생하는 오류 해결하기 (0) | 2025.04.02 |
[ Troubleshooting🛠️ ] 패키지 매니저 충돌 트러블 슈팅 npm, yarn, pnpm 간의 전환 (2) | 2025.04.01 |
Next.js의 Link 컴포넌트와 target="_blank" 속성 이해하기 (0) | 2025.03.31 |
[ Troubleshooting🛠️ ] Zod 스키마 검증과 데이터 처리 (0) | 2025.03.28 |