온보딩 UX와 점진적 공개 패턴: 사용자를 단계별로 안내하기

디자인

온보딩 UX점진적 공개사용자 경험프로덕트 투어신규 사용자

이 글은 누구를 위한 것인가

  • 신규 사용자의 초기 이탈을 줄이려는 팀
  • 복잡한 기능을 단계별로 소개하는 온보딩을 설계하는 개발자
  • 온보딩 완료율을 측정하고 개선하려는 팀

들어가며

"기능이 너무 많아서 어디서부터 시작해야 할지 모르겠어요" — 이것이 신규 사용자 이탈의 주원인이다. 점진적 공개(Progressive Disclosure)는 처음에는 핵심 기능만 보여주고, 숙련도가 높아질수록 더 많은 기능을 드러낸다.

이 글은 bluefoxdev.kr의 온보딩 UX 점진적 공개 가이드 를 참고하여 작성했습니다.


1. 온보딩 패턴 선택 가이드

[온보딩 유형]

1. 빈 상태 온보딩 (Empty State)
   - 데이터가 없을 때 안내 + CTA
   - 가장 비침습적
   - 예: "첫 상품을 추가해보세요 →"

2. 체크리스트 (Setup Checklist)
   - 완료해야 할 항목 목록
   - 게이미피케이션 효과 (진행률)
   - 예: Stripe, Notion 첫 설정

3. 프로덕트 투어 (Product Tour)
   - 단계별 툴팁/하이라이트
   - 스킵 가능해야 함
   - 첫 접속 1회만, 재실행 가능

4. 인라인 도움말
   - 아이콘 클릭 시 설명 팝오버
   - 필요할 때 볼 수 있음
   - 가장 비침습적

[점진적 공개 원칙]
  레벨 1: 핵심 기능만 (80% 사용자)
  레벨 2: 중급 기능 (고급 설정 토글)
  레벨 3: 전문가 기능 (숨김 메뉴)

  예: 이메일 에디터
  기본: 텍스트, 이미지, 버튼
  고급: HTML 편집, 사용자 정의 CSS

[완료율 측정]
  onboarding_started → step1_completed
  → step2_completed → onboarding_completed
  각 단계 이탈률 분석
  이탈 많은 단계 개선

2. 온보딩 구현

import { useState, useEffect } from 'react';

// 체크리스트 온보딩
interface OnboardingStep {
  id: string;
  title: string;
  description: string;
  action: string;
  actionUrl: string;
  completed: boolean;
}

function OnboardingChecklist({ steps, onStepComplete }: {
  steps: OnboardingStep[];
  onStepComplete: (stepId: string) => void;
}) {
  const completed = steps.filter(s => s.completed).length;
  const progress = Math.round(completed / steps.length * 100);

  return (
    <div style={{ border: '1px solid #e2e8f0', borderRadius: 12, padding: 24 }}>
      <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 }}>
        <h3 style={{ fontWeight: 600 }}>시작하기 ({completed}/{steps.length})</h3>
        <span style={{ color: '#10b981', fontWeight: 600 }}>{progress}%</span>
      </div>

      {/* 진행률 바 */}
      <div style={{ height: 6, background: '#e2e8f0', borderRadius: 3, marginBottom: 20 }}>
        <div style={{ width: `${progress}%`, height: '100%', background: '#10b981', borderRadius: 3, transition: 'width 0.4s ease' }} />
      </div>

      {steps.map(step => (
        <div key={step.id} style={{ display: 'flex', alignItems: 'flex-start', gap: 12, padding: '12px 0', borderBottom: '1px solid #f1f5f9' }}>
          <button
            onClick={() => !step.completed && onStepComplete(step.id)}
            aria-label={step.completed ? `${step.title} 완료` : `${step.title} 시작`}
            style={{
              width: 24, height: 24, borderRadius: '50%', flexShrink: 0,
              background: step.completed ? '#10b981' : 'white',
              border: `2px solid ${step.completed ? '#10b981' : '#d1d5db'}`,
              cursor: step.completed ? 'default' : 'pointer',
              display: 'flex', alignItems: 'center', justifyContent: 'center',
            }}
          >
            {step.completed && <span style={{ color: 'white', fontSize: 12 }}>✓</span>}
          </button>
          <div style={{ flex: 1 }}>
            <p style={{ fontWeight: 500, color: step.completed ? '#9ca3af' : '#111827', textDecoration: step.completed ? 'line-through' : 'none' }}>
              {step.title}
            </p>
            <p style={{ fontSize: 13, color: '#6b7280', marginTop: 2 }}>{step.description}</p>
            {!step.completed && (
              <a href={step.actionUrl} style={{ fontSize: 13, color: '#3b82f6', textDecoration: 'none', fontWeight: 500 }}>
                {step.action} →
              </a>
            )}
          </div>
        </div>
      ))}
    </div>
  );
}

// 프로덕트 투어 훅
function useProductTour(tourId: string) {
  const [currentStep, setCurrentStep] = useState(-1);
  const [isActive, setIsActive] = useState(false);

  useEffect(() => {
    const completed = localStorage.getItem(`tour:${tourId}`);
    if (!completed) {
      setIsActive(true);
      setCurrentStep(0);
    }
  }, [tourId]);

  const next = () => setCurrentStep(s => s + 1);
  const skip = () => {
    localStorage.setItem(`tour:${tourId}`, 'completed');
    setIsActive(false);
    setCurrentStep(-1);
  };
  const complete = () => {
    localStorage.setItem(`tour:${tourId}`, 'completed');
    setIsActive(false);
    analytics.track('onboarding_completed', { tourId });
  };

  return { currentStep, isActive, next, skip, complete };
}

const analytics = { track: (e: string, d: any) => {} };

마무리

온보딩의 성공은 "완료율"로 측정한다. 각 단계의 이탈률을 추적하고, 이탈이 많은 단계를 단순화한다. 체크리스트 패턴은 달성감을 주어 완료율이 높고, 프로덕트 투어는 항상 스킵 가능하게 해 강요받는 느낌을 주지 않아야 한다.