1. 문제 발생❓
데스크탑일 때는 괜찮은데, 모바일 환경에서 <SwiperSlide> 내부의 absolute 요소인 <StackImageWrapper>가 슬라이드 가로 스크롤 시 깜박이며 사라졌다 다시 나타나는 현상이 발생했습니다.
2. 원인 추론 🔎
< 수정 전 전체 코드 >
import styled from '@emotion/styled';
import { DESKTOP } from '@/styles/themes';
export const Background = styled.section`
width: 100%;
max-width: 820px;
display: flex;
flex-direction: column;
gap: 16px;
${DESKTOP} {
gap: 20px;
}
`;
export const SwiperButton = styled.button<{ isDisabled: boolean }>`
cursor: ${({ isDisabled }) => (isDisabled ? 'not-allowed' : 'pointer')};
display: flex;
justify-content: center;
align-items: center;
opacity: ${({ isDisabled }) => (isDisabled ? 0.3 : 1)};
background-color: transparent;
`;
export const CardWrapper = styled.div`
width: 320px;
display: flex;
flex-direction: column;
gap: 16px;
border-radius: 8px;
overflow: hidden;
`;
export const CardContent = styled.div`
width: 100%;
display: flex;
flex-direction: column;
`;
export const ImageWrapper = styled.div`
position: relative;
width: 100%;
height: 184.5px;
display: flex;
justify-content: center;
align-items: center;
`;
export const CardVideo = styled.video`
width: 100%;
height: 100%;
object-fit: cover;
`;
export const StackImageWrapper = styled.div`
width: 100%;
position: absolute;
bottom: 0;
left: 0;
display: flex;
padding: 12px 12px 0px 12px;
flex-direction: column;
align-items: flex-start;
background: linear-gradient(
180deg,
rgba(20, 22, 23, 0) 0%,
rgba(20, 22, 23, 0.8) 49%,
#141617 100%
);
`;
export const TitleWrapper = styled.div`
display: flex;
padding: 16px 12px 0px 12px;
flex-direction: column;
align-items: flex-start;
gap: 10px;
align-self: stretch;
`;
import './swiper.css';
import { Flex } from '@teamsparta/stack-flex';
import { ArrowBackLine, ArrowForwardLine } from '@teamsparta/stack-icons';
import { Text } from '@teamsparta/stack-text';
import { vars } from '@teamsparta/stack-tokens';
import Image from 'next/image';
import { useState } from 'react';
import type SwiperCore from 'swiper';
import { Swiper, SwiperSlide } from 'swiper/react';
import { LoggingClick } from '@/components/common/LoggingClick';
import Only from '@/components/common/Only';
import Toggle from '../Toggle';
import * as D from './data';
import * as S from './style';
const SWIPING_NUMBER = 1;
function SwiperWithToggle() {
const [swiper, setSwiper] = useState<SwiperCore>();
const [isSwiperBeginning, setIsSwiperBeginning] = useState(true);
const [isSwiperEnd, setIsSwiperEnd] = useState(false);
function handleClickSwiperBackButton() {
if (!swiper) {
return;
}
swiper.slideTo(swiper.activeIndex - SWIPING_NUMBER);
setIsSwiperBeginning(swiper.isBeginning);
setIsSwiperEnd(swiper.isEnd);
}
function handleClickSwiperForwardButton() {
if (!swiper) {
return;
}
swiper.slideTo(swiper.activeIndex + SWIPING_NUMBER);
setIsSwiperBeginning(swiper.isBeginning);
setIsSwiperEnd(swiper.isEnd);
}
function handleSlideChange() {
if (!swiper) {
return;
}
setIsSwiperBeginning(swiper.isBeginning);
setIsSwiperEnd(swiper.isEnd);
}
return (
<S.Background>
<Flex.Row fullWidth justify='between'>
<Text
as='h3'
font={{ mobile: 'subTitle2', desktop: 'subTitle2' }}
color={vars.neutral[10]}
>
결과물 더 보기
</Text>
<Only.Desktop>
<Flex.Row gap={12}>
<S.SwiperButton
disabled={isSwiperBeginning}
isDisabled={isSwiperBeginning}
onClick={handleClickSwiperBackButton}
>
<ArrowBackLine
size={20}
color={isSwiperBeginning ? vars.neutral[30] : vars.neutral[10]}
/>
</S.SwiperButton>
<S.SwiperButton
disabled={isSwiperEnd}
isDisabled={isSwiperEnd}
onClick={handleClickSwiperForwardButton}
>
<ArrowForwardLine
size={20}
color={isSwiperEnd ? vars.neutral[30] : vars.neutral[10]}
/>
</S.SwiperButton>
</Flex.Row>
</Only.Desktop>
</Flex.Row>
<Swiper
onSwiper={setSwiper}
onSlideChange={handleSlideChange}
slidesPerView='auto'
spaceBetween={24}
>
{D.SWIPER_CARD_DATA.map(
({
id,
videoSrc,
stackSrc,
title,
subTitle,
name,
position,
toggle,
}) => (
<SwiperSlide key={id}>
<S.CardWrapper>
<S.CardContent>
<S.ImageWrapper>
<S.CardVideo
src={videoSrc}
autoPlay
muted
loop
playsInline
/>
<S.StackImageWrapper>
<Image
src={stackSrc}
alt={subTitle}
width={102}
height={30}
/>
</S.StackImageWrapper>
</S.ImageWrapper>
<S.TitleWrapper>
<Text as='strong' font='subTitle3' color={vars.neutral[5]}>
{title}
</Text>
<Text as='p' font='captionM' color={vars.neutral[50]}>
{subTitle}
</Text>
<Text as='p' font='tag1M' color={vars.neutral[50]}>
{name} · {position}
</Text>
</S.TitleWrapper>
</S.CardContent>
<LoggingClick
logName='hhplus_mainPage_project_toggle_clickEvent'
data={{ name }}
>
<Toggle {...toggle} />
</LoggingClick>
</S.CardWrapper>
</SwiperSlide>
),
)}
</Swiper>
</S.Background>
);
}
export default SwiperWithToggle;
<수정 전 주요 코드 >
export const StackImageWrapper = styled.div`
width: 100%;
position: absolute;
bottom: 0;
left: 0;
display: flex;
padding: 12px 12px 0px 12px;
flex-direction: column;
align-items: flex-start;
background: linear-gradient(
180deg,
rgba(20, 22, 23, 0) 0%,
rgba(20, 22, 23, 0.8) 49%,
#141617 100%
);
`;
export const StackImageWrapper = styled.div`
width: 100%;
position: absolute;
bottom: 0;
left: 0;
display: flex;
padding: 12px 12px 0px 12px;
flex-direction: column;
align-items: flex-start;
background: linear-gradient(
180deg,
rgba(20, 22, 23, 0) 0%,
rgba(20, 22, 23, 0.8) 49%,
#141617 100%
);
`;
< 원인 추론 >
- <StackImageWrapper>는 absolute 포지션이며 부모 요소의 overflow: hidden 내에 존재
- 브라우저가 레이아웃을 계산하고 다시 화면을 그릴 때, 이런 요소들은 GPU로 분리하지 않으면 render pipeline 상에서 누락되거나 깜박일 수 있음
- 특히 모바일 브라우저(iOS Safari, 모바일 Chrome 등)는 성능 최적화를 위해 일부 요소를 일시적으로 생략하거나 합성을 늦추는 경우가 있음
➡️ GPU 레이어로 분리되어 있지 않다면, 스크롤 시 렌더링 타이밍 문제로 인해 요소가 깜박일 수 있음!!
3. 해결 과정 📋
try 1 : z-index 추가
export const StackImageWrapper = styled.div`
...
z-index: 10;
`;
- ✅목적 : stacking order 문제로 인한 가려짐을 방지하려고 함
- ❌실패 : 깜박임 증상은 그대로
try 2: <Image priority /> 설정
<Image
src={stackSrc}
alt={subTitle}
width={102}
height={30}
priority
/>
- ✅목적 : next/Image가 이미지를 미리 로드하게 하여 깜박임 방지 기대
- ❌실패 : 이미지 로딩과 깜박임은 별개의 문제였음
try 3: Swiper 설정 개선
<Swiper
...
watchSlidesProgress={true}
updateOnWindowResize={true}
observer={true}
observeParents={true}
/>
- ✅목적 : 슬라이드 위치나 DOM 변경 감지를 통해 상태 갱신 문제 해결 기대
- ❌실패 : Swiper 자체 문제는 아니었기 때문에 효과 없음
try 4 : GPU 레이어 강제 분리 (성공!)
export const StackImageWrapper = styled.div`
...
will-change: transform;
transform: translateZ(0);
`;
- 💡 원리:
- translateZ(0) => 해당 요소를 GPU 레이어로 분리
- will-change: transform => 브라우저에 미리 GPU 최적화 힌트 제공
- 🎯 효과:
- 깜박임 완전히 제거됨
- 스크롤 중에도 부드럽고 안정적으로 렌더링
🚀 잠깐만 여기서 GPU 가속이란?
✅ GPU란?
- GPU(Graphics Processing Unit): 주로 이미지, 애니메이션, 영상 렌더링을 빠르게 처리하는 장치
- CSS에서 특정 속성(transform, opacity, filter 등)을 사용할 때 브라우저가 해당 요소를 GPU에서 처리하도록 할 수 있음
✅ 왜 GPU로 분리하는가?
- GPU는 병령 연산에 특화되어 있어서 스크롤 중에도 부드러운 렌더링 가능
- transform: translateZ(0)을 적용하면, 브라우저는 이 요소를 GPU 레이어로 따로 분리해서 처리함
- 그 결과:
- 레이아웃 계산 지연에 영향받지 않고
- 깜박임 없이 항상 그려짐
- 전체 UI 퍼포먼스도 개선됨
4. 결론 💬
이번 트러블슈팅을 통해 렌더링 타이밍과 GPU 가속 최적화의 중요성을 체감했습니다. UI 깜박임 문제가 무조건 DOM 구조나 로딩 문제는 아니며, CSS 속성 하나로도 해결되는 GPU 레벨의 문제일 수 있다는 것을 배웠습니다!
'인턴' 카테고리의 다른 글
[ Troubleshooting🛠️ ] 여러개의 Atom을 하나로 통합 (0) | 2025.04.30 |
---|---|
[ Troubleshooting🛠️ ] 모달 표시 문제 해결 (0) | 2025.04.29 |
[ Troubleshooting🛠️ ] sticky 포지션과 overflow: hidden 충돌 해결하기 (0) | 2025.04.21 |
Zustand 완전 정복 가이드 (0) | 2025.04.15 |
[성능 최적화⚙️] Set 활용해보기 (1) | 2025.04.14 |