'use client';
import FirstSection from '@/components/pages/Home/FirstSection';
import SecondSection from '@/components/pages/Home/SecondSection';
import { Profiler, useRef } from 'react';
export default function Home() {
const renderCount = useRef(0);
const handleRender = (id, phase, actualDuration) => {
renderCount.current += 1;
console.log(`🔄 ${id} 리렌더링 횟수: ${renderCount.current}`);
console.log(`⏱️ 렌더링 시간: ${actualDuration}ms`);
};
return (
<>
<Profiler id="MyComponent" onRender={handleRender}>
<FirstSection />
</Profiler>
<SecondSection />
</>
);
}
기존 코드
'use client';
import styled from '@emotion/styled';
import PromotionVideo from '@/assets/Home/promotion-video.mp4';
import { useEffect, useRef, useState } from 'react';
export default function FirstSection() {
const containerRef = useRef<HTMLDivElement>(null);
const [scrollPadding, setScrollPadding] = useState(0);
useEffect(() => {
let rafId: number;
const handleScroll = () => {
rafId = requestAnimationFrame(() => {
const scrollY = window.scrollY;
if (scrollY <= 100) {
const paddingPercentage = scrollY / 100;
setScrollPadding(paddingPercentage);
} else if (scrollY > 100) {
setScrollPadding(1);
}
});
};
window.addEventListener('scroll', handleScroll);
return () => {
window.removeEventListener('scroll', handleScroll);
cancelAnimationFrame(rafId);
};
}, []);
return (
<Container ref={containerRef} $scrollPadding={scrollPadding}>
<VideoWrapper $scrollPadding={scrollPadding}>
<Video autoPlay muted loop>
<source src={PromotionVideo} type="video/mp4" />
</Video>
</VideoWrapper>
</Container>
);
}
const Container = styled.div<{ $scrollPadding: number }>`
box-sizing: border-box;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
width: 100vw;
padding: 0px ${(props) => props.$scrollPadding * 80}px ${(props) => props.$scrollPadding * 80}px
${(props) => props.$scrollPadding * 80}px;
transition: padding 0.3s ease;
`;
const VideoWrapper = styled.div<{ $scrollPadding: number }>`
display: flex;
justify-content: center;
align-items: center;
height: 100%;
width: 100%;
border-radius: ${(props) => props.$scrollPadding * 50}px;
overflow: hidden;
transition: border-radius 0.3s ease;
`;
const Video = styled.video`
display: flex;
justify-content: center;
align-items: center;
height: 100%;
width: 100%;
object-fit: fill;
`;
개선된 코드
React 상태를 사용하지 않고 CSS 변수를 직접 설정하는 것도 중요
css in js를 사용할 때 리렌더링이 일어나면 클래스네임이 갱신되서 CSSOM트리에서 찾는 과정때문에 시간이 많이 소요된다.
'use client';
import styled from '@emotion/styled';
import PromotionVideo from '@/assets/Home/promotion-video.mp4';
import { useEffect, useRef } from 'react';
export default function FirstSection() {
// DOM 요소에 직접 접근하기 위한 ref 사용
const containerRef = useRef(null);
const videoContainerRef = useRef(null);
const scaleWrapperRef = useRef(null);
useEffect(() => {
let rafId;
const container = containerRef.current;
const videoContainer = videoContainerRef.current;
const scaleWrapper = scaleWrapperRef.current;
if (!container || !videoContainer || !scaleWrapper) return;
// 스크롤 이벤트 핸들러
const handleScroll = () => {
rafId = requestAnimationFrame(() => {
const scrollY = window.scrollY;
// 0에서 1 사이의 값으로 정규화
const progress = Math.min(1, scrollY / 100);
// 반올림하여 미세한 변화 무시 (성능 최적화)
const roundedProgress = Math.round(progress * 100) / 100;
// React 상태를 사용하지 않고 CSS 변수를 직접 설정
container.style.setProperty('--scroll-progress', roundedProgress);
// transform 속성을 직접 설정 (리렌더링 없이 애니메이션 적용)
const scale = 1 - roundedProgress * 0.1;
scaleWrapper.style.transform = `scale(${scale})`;
videoContainer.style.transform = `scale(${scale})`;
// clip-path 업데이트
const insetValue = roundedProgress * 80;
const borderRadius = roundedProgress * 50;
videoContainer.style.clipPath = `inset(${insetValue}px round ${borderRadius}px)`;
});
};
window.addEventListener('scroll', handleScroll);
return () => {
window.removeEventListener('scroll', handleScroll);
cancelAnimationFrame(rafId);
};
}, []);
return (
<Container ref={containerRef}>
<ScaleWrapper ref={scaleWrapperRef}>
<VideoContainer ref={videoContainerRef}>
<Video autoPlay muted loop>
<source src={PromotionVideo} type="video/mp4" />
</Video>
</VideoContainer>
</ScaleWrapper>
</Container>
);
}
// 스타일 컴포넌트에서는 이제 props를 사용하지 않음 (리렌더링 방지)
const Container = styled.div`
box-sizing: border-box;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
width: 100vw;
overflow: hidden;
/* CSS 변수 초기화 */
--scroll-progress: 0;
`;
const ScaleWrapper = styled.div`
height: 100%;
width: 100%;
display: flex;
justify-content: center;
align-items: center;
will-change: transform;
/* 초기 transform 설정 - JavaScript에서 직접 업데이트 */
transform: scale(1);
`;
const VideoContainer = styled.div`
height: 100%;
width: 100%;
overflow: hidden;
border-radius: 50px;
will-change: transform, clip-path;
transition: clip-path 0.3s ease;
/* 초기값 설정 - JavaScript에서 직접 업데이트 */
transform: scale(1);
clip-path: inset(0px round 0px);
`;
const Video = styled.video`
height: 100%;
width: 100%;
object-fit: fill;
transform: translateZ(0);
will-change: transform;
`;