인턴

[ Troubleshooting🛠️ ] 모바일 Swiper에서 absolute 요소가 깜박이는 현상 해결

choijming21 2025. 4. 28. 15:48

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 레벨의 문제일 수 있다는 것을 배웠습니다!