Gu Doyoon

튜토리얼 UI로 사용자 이탈 방지하기

1/22/2026 작성

서론

Coko 프로젝트를 진행하며 신규 사용자를 위한 튜토리얼 UI Flow를 직접 제안하고 구현했습니다. 해당 UI 구현과 트러블 슈팅을 기록하기 위해 글을 작성해 보려 합니다 ㅎㅎ

🤔 분리된 컴포넌트, 랜더링하지?

튜토리얼은 단순히 정적인 이미지를 보여주는 것이 아닙니다. '학습하기 버튼', '마이페이지 아이콘'처럼 실제 렌더링된 컴포넌트 위에 정확히 올라가 시각적인 가이드를 제공해야 합니다.

어떻게 튜토리얼 컴포넌트가 다른 컴포넌트의 위치(좌표, 크기)를 정확히 알고 그 위에 오버레이를 그릴 수 있을까가 제일 큰 고민이였습니다.

🚀 1차 접근: getElementById 사용해보기

가장 먼저 떠올린 간단한 방법은 id를 활용하기였다.
가이드할 대상 요소에 고유한 id를 부여하고, 튜토리얼 컴포넌트의 useEffect 내에서 document.getElementById를 사용해 해당 DOM 요소를 찾아 rect를 수집하는 방법이였다.

이는 옳은 방법일까?

하지만 이 접근 방식은 React의 선언적인 패러다임과 맞지 않았고, 몇 가지 명확한 한계점을 가지고 있었는데

그렇게 어떻게 해결할까 의견을 들어보기 위해 코멘트를 남겼는데..

멘토분이 callbackRef를 이용해보는건 어떤지 제안해주셨다.

callbackRef는 컴포넌트가 렌더링 된 후 실행되기 때문에 안정적으로 rect 요소를 수집할 수 있기에 해당 방법을 사용하여 구현해보기로 했다.

callbackRef 사용하기

이 문제를 해결하기 위해서 다음과 같은 FLOW를 그려보았다.

  1. 튜토리얼에서 가이드할 UI 요소의 DOMRect (위치와 크기 정보)를 callbackRef 로 안정적으로 수집한다.

  2. 수집된 DOMRect 정보를 어떤 컴포넌트에서든 참조할 수 있도록 전역 상태로 관리한다.

구현 해보기

export const useElementRect = () => { const { setRect } = useRectStore(); // callbackRef생성 const getClientRectCallbackRef = useCallback( <T extends HTMLElement>(node: T | null) => { if (!node) return; const rect = node.getBoundingClientRect(); const id = node.id; setRect(id, rect); }, [] ); return { getClientRectCallbackRef }; };

해당 요소의 ref를 인자로 받아 rect를 전역 상태로 수집하는 커스텀 훅을 구현해봤습니다.

순서대로 어떻게 보여줄까?

마지막으로, 수집된 DOM 정보를 바탕으로 실제 튜토리얼 UI를 순서대로 보여주기 위해 이전에 구현했던 useFunnel 훅을 활용하여 순서대로 렌더링했다.

// src/features/intro/ui/LearnTutorialContainer.tsx const STEPS = ['학습하기', '퀴즈풀기', '마이페이지'] as const; export default function LearnTutorialContainer() { const { Funnel, setStep } = useFunnel(STEPS[0]); return ( <Funnel> {TUTORIAL_STEP.map(step => ( <Funnel.Step name={step.name} key={step.name}> <FocusedItem id={step.id} onNext={() => setStep(step.nextStep)} description={step.description} /> </Funnel.Step> ))} </Funnel> ); }

수집한 rect에서 width, height,등 위치를 잡는데 필요한 요소들을 추출하여 특정 부분을 정확히 하이라이트 해주는 컴포넌트에게 전달해주어 특정 부분을 하이라이팅 해주는 기능을 완성하였다!!

const computedStyle = rect ? { width: `${rect.width}px`, height: `${rect.height}px`, top: `${rect.top}px`, left: `${rect.left}px`, } : undefined; //.... <FocusedItemDiv style={computedStyle} />

추가적인 문제 발생?!

하지만 위 사진을 보면 코끼리가 동적으로 나타나야 한다는 기획이 남아있게 되는데

기획된 사진을 보면 코끼리를 동적으로 렌더링 해야했다.

export const calculateTutorialPopupPosition: FindMostSpaciousDirection = rect => { //모바일인지 체크하는 플레그 isMobile 선언 const isMobile .... if(isMobile) // 모바일 환경에서는 상, 하 여백이 많은 쪽으로 rect값을 계산! else // 데스크톱 환경에서는 //상,하,좌,우 여백이 많은 곳으로 rect 계산! //계산된 위치를 반환! return position; };

위와같이 rect값과 현재 스크린 사이즈를 고려하여 렌더링될 코끼리의 위치를 계산해주는 유틸 함수를 작성하여 스크린 사이즈에 맞게 렌더링 될 수 있도록 해주었다.

결과 및 회고


사용자에 디스플레이 사이즈는 모두 다르기 때문에 개발자가 일일히 위치를 알려주는게 아닌 동적으로 focus되는 튜토리얼 UI를 구현해봤습니다.

이번 커스텀 훅 / 컴포넌트 개발을 통해 팀원이 다른 페이지의 튜토리얼 UI를 개발할 때 도움이 많이 되었다고 하니 기분이 많이 좋네요ㅎㅎㅎ
팀원의 튜토리얼 페이지 개발