Notice
Recent Posts
Recent Comments
Link
«   2026/07   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
Tags
more
Archives
Today
Total
관리 메뉴

mimizae 님의 블로그

ticker 구현기 본문

FE

ticker 구현기

mimizae 2026. 2. 9. 15:43

37기 DIVE SOPT에서 진행한 프로젝트 CareNA 프로젝트에서 맡은 부분을 구현한 과정을 담은 글입니다! 🙌🏻


💡 내가 구현해야 할 UI

내가 맡은 공통 컴포넌트 중 하나인, ticker는 사용자의 건강검진 상태에 맞게 맞춤 건강 팁들을 홈에서 세로로 자동 스크롤 되며 보여주는 기능을 갖고 있다.

피그마 캡쳐본

 

이 컴포넌트를 처음 보고 vetical로 자동으로 올라가는 걸 어떻게 구현할지 고민하다가 자동으로 올라간다는 것과 방향만 다를 뿐 이게 가로였다면... 캐러셀 그 자체인 것!!을 깨닫고 캐러셀의 원리를 공부하고 로직을 구성했다!


💡 기능 구현

이 ticker의 핵심 원리는 위치 이동 + 상태 관리이다.

 

기본 아이디어는 다음과 같다!

  • 여러 개의 텍스트를 세로로 쭉 나열해 두고
  • 지금 보여줘야 할 요소의 index를 state로 관리
  • translateY를 이용해 컨테이너 자체를 위로 이동시켜 마치 한 줄씩 위로 올라가는 것처럼 보이게 하자.

1️⃣ 팁 배열 확장 (무한 루프를 위한 준비)

const tips = ["팁1", "팁2", "팁3"];
const extendedTips = ["팁1", "팁2", "팁3", "팁1"]; // 0번째 index를 확장한 버전

 

우선 마지막에 첫 번째 팁을 한 번 더 복제한 배열을 만든다.

이 복제된 팁은 새로운 데이터를 보여주기 위한 게 아니라 마지막에서 자연스럽게 한 칸 더 내려가는 애니메이션을 만들기 위한 장치이다!!

 

2️⃣ 자동으로 index를 증가시키는 로직

useEffect(() => {
  const autoScrollInterval = setInterval(() => {
    setIsTransitionEnabled(true);
    setVisibleIndex((prev) => prev + 1);
  }, 3000);
  
  return () => clearInterval(autoScrollInterval);
}, []);

 

3초마다 visibleIndex를 1씩 증가시킨다. 이때는 항상 transition을 켜둔 상태로 이동한다.

결과적으로 translateY(-visibleIndex * ITEM_HEIGHT) 값이 바뀌면서 한 줄씩 위로 스르륵 이동한다!!

 

3️⃣ 마지막에 도달했을 때 순간이동 처리

 

이 부분이 Ticker의 핵심 포인트이다!!

visibleIndex === tips.length가 되는 순간, 즉 팁 배열의 마지막 아이템까지 보여준 순간은 복제된 팁 1이 화면에 보인 상태이다.

이 상태에서 transition을 끄고 index만을 강제로 0으로 되돌린다.

이때, transition이 꺼져 있고, index만 수정하는 것이기 때문에 사용자는 이 위치 변경을 전혀 인식하지 못하며!!!!! 실제로 화면에서는 다음과 같이 보일 것이다.

 

0px
↓
-35px (한 줄 아래로 -> 팁 2로)
↓
-70px (두 줄 아래로 -> 팁 3으로)
↓
-105px (복제 팁1으로 )
↓
(순간이동...)
0px (강제로, index만 변경!! 화면은 여전히 복제된 팁 1이 보인다)
↓
-35px (한 줄 아래로 -> 팁 2로)

 

복제된 팁 1 → 진짜 팁 1로 바뀌는 순간 화면에는 동일한 내용이 보이기 때문에 사용자는 위로 튕겨 다시 올라갔다는 느낌 없이 계속 아래로 흐르는 것처럼 인식하게 된다!!

(index가 교체될 뿐, 애니메이션은 없는 상태)

그 다음 interval에서 index가 다시 증가하면서 자연스럽게 팁 2 → 팁 3 → … 순서로 이어진다!!!


💡 UI

<div
  className={cn(
    "will-change-transform",
    isTransitionEnabled && "transition-transform duration-400 ease-in-out",
  )}
  style={{ transform: `translateY(-${visibleIndex * ITEM_HEIGHT}px)` }}>
    {extendedTips.map((tip) => (
      <div
        key={tip.id}
        className="h-[3.5rem] overflow-hidden text-ellipsis whitespace-nowrap">

 

💭 모든 문자열을 세로로 쭉 늘어놓고, 컨테이너를 통째로 위로 밀어 올리면서 input 창 안에 보이는 한 줄만 바꿔 보여주는 구조

 

1️⃣ 위치 이동 방식 (translateY)

transform: translateY(-${visibleIndex * ITEM_HEIGHT}px)

 

각 팁은 고정된 높이(ITEM_HEIGHT)를 가지고 세로로 나열되어 있다.

현재 보여줄 팁의 index(visibleIndex)에 따라 translateY(-index * height) 값을 계산해서 컨테이너 전체를 아래로 이동시킨다!

 

실제 DOM 구조는 그대로 두고, 위치만 transform으로 이동하기 때문에 리플로우 리페인트를 발생시키지 않고 합성 단계에서만 처리되어 성능상 유리하다고 판단했다!

 

2️⃣ inline style을 사용한 이유

style={{ transform: `translateY(-${visibleIndex * ITEM_HEIGHT}px)` }}

 

이 부분은 의도적으로 inline style을 사용했다.

translateY 값은 visibleIndex * ITEM_HEIGHT처럼 런타임 계산이 필요한 값인데, Tailwind 유틸 클래스는 정적인 값만 처리 가능하기 때문에 이 경우 순수 유틸로 표현하기가 어려웠다.

 

따라서 위치 이동처럼 상태에 따라 계속 바뀌는 값은 inline style로 처리하는 것이 가장 명확하고 안전한 방식이라고 판단했다!!


💡 구현 결과

import { Ticker } from "@/shared/ui/ticker/ticker";

const tips = [
  {
    id: 1,
    title: '건강 팁 한 줄 쪼로록!'
  },
  {
    id: 2,
    title: '건강 팁이 한 줄이 넘어갈 경우에는 말 줄임표를 쓸래 말래 쓸래말래쓸래말래' 
  },
  {
    id: 3,
    title: '단 거 적당히 드세요 ㅋㅋ'
  },
]

export const HomePage = () => {
  return (
    <div className="p-[2rem] bg-primary-50">
      <h1>CareNA</h1>
      <Ticker tips={tips}/>
     </div>
  );
};


💡ETC

작업 PR 링크