웹에서 화면이 전환될 때 벌어지는 일을 아시나요? 🤯

타락한스벨트전도사·2025년 7월 26일
55
post-thumbnail

웹에서 화면이 전환될 때 벌어지는 일:
1. 현재 DOM 트리 파괴 ❌
2. 새로운 DOM 트리 생성 🆕
3. 리페인트 & 리플로우 🎨

이 0.03초 동안 사용자의 뇌는 '맥락 상실'을 경험합니다.
앱에서는 이미 해결한 문제인데, 웹은 왜 아직일까요?

시니어 프론트엔드 개발자들은 이 문제를 알고 있습니다. 그리고 해결하려 노력합니다.
하지만 기존 솔루션들은 한계가 명확했죠.

이 글에서는 SSGOI(https://ssgoi.dev)가 어떻게 이 문제를 해결했는지, 그 동작 원리와 구현 과정을 깊이 있게 살펴보겠습니다.

https://ssgoi.dev

🔍 DOM 애니메이션의 근본적인 문제

프론트엔드 개발자라면 한 번쯤 이런 고민을 해보셨을 겁니다:

"페이지가 전환될 때 요소가 사라지기 전에 애니메이션을 주고 싶은데..."

간단해 보이지만, 실제로는 복잡한 문제입니다:

// 이상적인 코드 (하지만 동작하지 않음)
if (shouldRemove) {
  element.classList.add('fade-out'); // 애니메이션 추가
  await sleep(300); // 애니메이션 대기
  element.remove(); // 그 다음 제거
}

문제는 React, Vue 같은 프레임워크는 선언적으로 동작한다는 점입니다. 컴포넌트가 언마운트되면 DOM은 즉시 사라집니다. 애니메이션을 기다려주지 않죠.

🧩 SSGOI의 핵심 아이디어: DOM 생명주기 가로채기

SSGOI의 핵심은 DOM 요소의 생성과 소멸 시점을 가로채는 것입니다. 이를 통해 애니메이션을 삽입할 타이밍을 확보합니다.

┌─────────────────────────────────────────────────────┐
│                  일반적인 DOM 생명주기                │
├─────────────────────────────────────────────────────┤
│                                                     │
│  생성 ──────> 렌더링 ──────> 언마운트 ──────> 제거    │
│                                                     │
└─────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────┐
│                 SSGOI의 DOM 생명주기                 │
├─────────────────────────────────────────────────────┤
│                                                     │
│  생성 ──┬──> [IN 애니메이션] ──> 렌더링              │
│         │                                           │
│         └──> 언마운트 ──> [OUT 애니메이션] ──> 제거  │
│                                                     │
└─────────────────────────────────────────────────────┘

🤔 기존 방식의 한계와 SSGOI의 해결책

Chrome의 View Transition API의 한계

// 크롬에서만 동작하는 코드
if (document.startViewTransition) {
  document.startViewTransition(() => {
    // 페이지 전환
  });
}
// Firefox, Safari 사용자는? 🤷‍♂️

View Transition API는 멋지지만 크롬 전용입니다. 전체 사용자의 30%는 이 경험을 할 수 없죠.

CSS 트릭만으로는 부족했던 이유

.page-enter {
  animation: fadeIn 0.3s ease-in;
}

CSS 애니메이션은 간단하지만:

  • 페이지 간 상태 공유 불가능
  • 동적인 전환 효과 구현 어려움
  • 스크롤 위치, 요소 위치 추적 불가

프레임워크별 라우팅 시스템의 차이

  • React Router의 <Link>
  • Vue Router의 router-link
  • Next.js의 next/link
  • SvelteKit의 goto()

각자 다른 방식으로 동작하는데, 이걸 하나로 통합하는 게 쉬울까요?

🏗️ SSGOI의 아키텍처: 3층 구조

1. Core Layer: 애니메이션 엔진

SSGOI의 핵심은 스프링 물리 기반 애니메이션 엔진입니다:

// 실제 SSGOI의 타입 정의
interface TransitionConfig {
  // 스프링 물리 설정
  spring?: {
    stiffness: number;  // 강성: 얼마나 빠르게 목표에 도달할지
    damping: number;    // 감쇠: 얼마나 부드럽게 멈출지
  };
  
  // 매 프레임마다 호출되는 콜백
  tick?: (progress: number) => void;
  
  // 애니메이션 시작 전 준비
  prepare?: (element: HTMLElement) => void;
  
  // 생명주기 훅
  onStart?: () => void;
  onEnd?: () => void;
}

핵심 인사이트: progress는 단순한 0-1 값이 아닙니다. 스프링 물리 엔진이 생성하는 자연스러운 곡선입니다.

progress 값의 변화 (스프링 물리)
┌─────────────────────────────────┐
│ 1.2 ┤     ╭─╮                  │  오버슈트
│ 1.0 ┤   ╭─╯  ╰─────────        │  (자연스러운 바운스)
│ 0.8 ┤  ╱                       │
│ 0.6 ┤ ╱                        │
│ 0.4 ┤╱                         │
│ 0.2 ┤                          │
│ 0.0 ┴────────────────────      │
└─────────────────────────────────┘
      시간 →

2. Transition Callback Layer: 생명주기 관리

이 레이어가 SSGOI의 핵심 마법이 일어나는 곳입니다:

// 실제 구현의 핵심 로직
export function createTransitionCallback(
  getTransition: () => Transition,
  options?: { onCleanupEnd?: () => void }
): TransitionCallback {
  let currentAnimation: { animator: Animator; direction: "in" | "out" } | null = null;
  let currentClone: HTMLElement | null = null;
  
  return (element: HTMLElement | null) => {
    if (!element) return;
    
    // 1. 요소가 마운트될 때: IN 애니메이션 실행
    runEntrance(element);
    
    // 2. cleanup 함수 반환 (React의 useEffect cleanup과 유사)
    return () => {
      // 3. 요소가 언마운트될 때: 복제본 생성 후 OUT 애니메이션
      const cloned = element.cloneNode(true) as HTMLElement;
      runExitTransition(cloned);
    };
  };
}

핵심 트릭: 요소가 제거될 때 복제본을 생성하여 애니메이션을 계속 진행합니다!

언마운트 시 동작 과정:
┌────────────────────────────────────────────────┐
│ 1. React가 컴포넌트 언마운트 시작              │
│    └─> cleanup 함수 호출                      │
│                                               │
│ 2. SSGOI가 DOM 복제본 생성                     │
│    └─> 원본과 동일한 위치에 삽입              │
│                                               │
│ 3. 원본 DOM 제거 (React에 의해)                │
│    └─> 사용자는 복제본을 보고 있음            │
│                                               │
│ 4. 복제본에서 OUT 애니메이션 실행              │
│    └─> 애니메이션 완료 후 복제본도 제거        │
└────────────────────────────────────────────────┘

3. Framework Adapter Layer: 프레임워크별 통합

각 프레임워크는 DOM을 다루는 방식이 다릅니다. SSGOI는 이를 추상화합니다:

// React Adapter
export function transition(options: TransitionOptions) {
  const callback = createTransitionCallback(/* ... */);
  
  // React의 ref 패턴 활용
  return (element: HTMLElement | null) => {
    if (element) {
      // React는 ref가 변경될 때마다 이전 cleanup을 호출
      return callback(element);
    }
  };
}

// Svelte Adapter  
export function transition(node: HTMLElement, options: TransitionOptions) {
  const cleanup = createTransitionCallback(/* ... */)(node);
  
  // Svelte의 action 패턴
  return {
    destroy() {
      cleanup?.();
    }
  };
}

💡 실제 동작 예시: 문서 간 스크롤 전환

SSGOI 문서 사이트에서 실제로 사용하는 스크롤 전환을 예시로 살펴보겠습니다:

설정 코드

// 문서 네비게이션 설정
const transitions = [
  {
    from: '/docs/introduction',
    to: '/docs/quick-start',
    transition: scroll({ 
      direction: 'up',  // 다음 페이지로 갈 때는 위로
      spring: { stiffness: 20, damping: 7 }  // 부드러운 스프링 설정
    })
  },
  {
    from: '/docs/quick-start',
    to: '/docs/introduction',
    transition: scroll({ 
      direction: 'down',  // 이전 페이지로 갈 때는 아래로
      spring: { stiffness: 20, damping: 7 }
    })
  }
];

동작 과정

시간 흐름 →
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

T0: Introduction 페이지 (현재)
┌─────────────────────────┐
│ # SSGOI 소개            │
│                         │
│ SSGOI는 웹에서 네이티브 │
│ 앱과 같은 자연스러운... │
│                         │
│ [다음: Quick Start →]   │ ← 사용자 클릭
└─────────────────────────┘

T1: 전환 시작 (DOM 복제 발생)
┌─────────────────────────┐ 
│ # SSGOI 소개 [복제본]   │ ← OUT 애니메이션
│                         │    translateY: 0 → -100%
│ SSGOI는 웹에서 네이티브 │
│ 앱과 같은 자연스러운... │
└─────────────────────────┘
              ↑ 위로 스크롤되며 사라짐

T2: 새 페이지 진입
              ↓ 아래에서 올라옴
┌─────────────────────────┐
│ # Quick Start           │ ← IN 애니메이션
│                         │    translateY: 100% → 0
│ SSGOI를 시작하는 가장   │
│ 빠른 방법을 알아봅시다  │
└─────────────────────────┘

T3: 전환 완료
┌─────────────────────────┐
│ # Quick Start           │
│                         │
│ SSGOI를 시작하는 가장   │
│ 빠른 방법을 알아봅시다  │
│                         │
│ [← 이전] [다음 →]       │
└─────────────────────────┘

핵심 구현 원리

// scroll 전환 효과의 실제 구현
export const scroll = (options: ScrollOptions = {}): SggoiTransition => {
  const isUp = options.direction === "up";
  
  return {
    in: (element) => ({
      spring: { stiffness: 20, damping: 7 },
      tick: (progress) => {
        // progress: 0 → 1
        const translateY = isUp 
          ? (1 - progress) * 100   // 100% → 0 (아래에서 위로)
          : (1 - progress) * -100; // -100% → 0 (위에서 아래로)
        element.style.transform = `translateY(${translateY}%)`;
      }
    }),
    out: (element) => ({
      prepare: prepareOutgoing,  // 복제본을 절대 위치로 고정
      tick: (progress) => {
        // progress: 1 → 0  
        const translateY = isUp
          ? (1 - progress) * -100  // 0 → -100% (위로 사라짐)
          : (1 - progress) * 100;  // 0 → 100% (아래로 사라짐)
        element.style.transform = `translateY(${translateY}%)`;
      }
    })
  };
};

이렇게 마치 연속된 문서를 스크롤하듯 자연스럽게 페이지가 전환됩니다!

🔧 성능 최적화 전략

1. 스프링 물리 vs CSS Transition

왜 CSS transition 대신 JavaScript 스프링 물리를 선택했을까요?

/* CSS Transition의 한계 */
.fade-out {
  transition: opacity 300ms ease-out;
  opacity: 0;
}

문제점:

  • 중간에 멈추거나 방향을 바꿀 수 없음
  • 한 번 시작하면 끝까지 가야 함
  • 복잡한 곡선 표현 불가
// 스프링 물리의 장점
const animator = new Animator({
  spring: { stiffness: 300, damping: 30 },
  onUpdate: (progress) => {
    // 언제든 중단, 역방향, 속도 변경 가능
    element.style.opacity = progress;
  }
});

// 사용자가 빠르게 클릭하면?
animator.reverse(); // 즉시 반대 방향으로

2. 프레임 드롭 방지

SSGOI는 requestAnimationFrame을 통해 브라우저의 렌더링 주기와 동기화됩니다:

// Popmotion 라이브러리 활용
import { animate } from "popmotion";

// 60fps 유지를 위한 최적화
this.controls = animate({
  from: this.currentValue,
  to: target,
  velocity: this.velocity * 1000,
  stiffness: this.options.spring.stiffness,
  damping: this.options.spring.damping,
  
  onUpdate: (value: number) => {
    // RAF와 동기화되어 프레임 드롭 최소화
    this.currentValue = value;
    this.options.onUpdate(value);
  }
});

🚀 실전 활용: 고급 패턴과 팁

1. 상태 기반 전환 전략

SSGOI의 강력한 기능 중 하나는 전환 전략(Transition Strategy)입니다:

// 애니메이션이 진행 중일 때 방향이 바뀌면?
const strategy = (context: StrategyContext) => ({
  async runIn(configs) {
    const { currentAnimation } = context;
    
    if (currentAnimation?.direction === "out") {
      // OUT 애니메이션 중이면 현재 상태에서 역방향
      return {
        config: await configs.in,
        state: currentAnimation.animator.getCurrentState(),
        direction: "backward",
        from: 1,
        to: 0
      };
    }
    
    // 기본: 0에서 1로
    return {
      config: await configs.in,
      state: { position: 0, velocity: 0 },
      direction: "forward",
      from: 0,
      to: 1
    };
  }
});

2. 조건부 애니메이션

// 모바일에서는 단순하게, 데스크톱에서는 화려하게
const responsiveTransition = {
  in: (element) => ({
    spring: {
      stiffness: window.innerWidth > 768 ? 300 : 500,
      damping: window.innerWidth > 768 ? 30 : 40
    },
    tick: (progress) => {
      if (window.innerWidth > 768) {
        // 데스크톱: 3D 회전 + 스케일
        element.style.transform = `
          perspective(1000px)
          rotateY(${90 * (1 - progress)}deg)
          scale(${0.8 + 0.2 * progress})
        `;
      } else {
        // 모바일: 단순 페이드
        element.style.opacity = progress;
      }
    }
  })
};

3. 공유 요소 애니메이션 (Hero Transition)

Instagram 스토리처럼 요소가 화면을 넘나드는 효과:

// 상품 이미지가 리스트에서 상세 페이지로 이동하는 효과
const heroTransition = {
  out: async (element) => {
    const rect = element.getBoundingClientRect();
    
    return {
      prepare: (el) => {
        // 절대 위치로 고정
        el.style.position = 'fixed';
        el.style.top = `${rect.top}px`;
        el.style.left = `${rect.left}px`;
        el.style.width = `${rect.width}px`;
        el.style.height = `${rect.height}px`;
      },
      tick: (progress) => {
        // 화면 중앙으로 이동하며 확대
        const scale = 1 + (2 - 1) * (1 - progress);
        const x = (window.innerWidth / 2 - rect.left - rect.width / 2) * (1 - progress);
        const y = (window.innerHeight / 2 - rect.top - rect.height / 2) * (1 - progress);
        
        element.style.transform = `
          translate(${x}px, ${y}px)
          scale(${scale})
        `;
      }
    };
  }
};

💭 마무리: 웹의 미래를 향해

SSGOI를 만들면서 깨달은 것은, 웹과 앱의 경계가 점점 흐려지고 있다는 점입니다.

과거에는 "웹은 문서, 앱은 애플리케이션"이라는 명확한 구분이 있었지만, 이제 웹도 충분히 풍부한 인터랙션을 제공할 수 있습니다.

핵심 교훈

  1. 선언적 UI의 한계를 극복하는 방법은 있다

    • DOM 복제, 생명주기 가로채기 등 창의적인 해결책
  2. 물리 기반 애니메이션이 자연스러움의 핵심

    • CSS보다 복잡하지만, 그만한 가치가 있음
  3. 프레임워크 중립적 설계의 중요성

    • 코어를 잘 설계하면 어떤 프레임워크든 지원 가능

다음 도전 과제

  • 더 많은 프레임워크 지원: 리액트, 스벨트를 넘어 현대 모든 웹프레임워크
  • 접근성 향상: 모션 감소 설정 지원

🚀 시작하기

# React
npm install @ssgoi/react

# Svelte  
npm install @ssgoi/svelte

# Vue (Coming Soon)
npm install @ssgoi/vue
// 단 3줄로 시작 +layout.tsx
import { Ssgoi } from '@ssgoi/react';
import { fade } from '@ssgoi/react/view-transitions';

<Ssgoi config={{ defaultTransition: fade() }}>
  {children}
</Ssgoi>

함께 만들어가요 🙏

GitHub: meursyphus/ssgoi

이 글이 여러분의 웹 개발 여정에 새로운 관점을 제공했기를 바랍니다.

웹을 더 부드럽게, 더 자연스럽게, 더 아름답게.

그것이 SSGOI의 미션입니다. 🎯

UTM 파라미터가 추가된 링크와 함께 블로그 글 마지막에 넣을 수 있는 소개 문구를 만들어드릴게요:


🔗 참고로 이력서 서비스 운용하고 있습니다. 한번 둘러봐주세용 👀✨

AI 이력서 분석 서비스: 여기!

PDF 업로드만으로 30초 안에 이력서 점수와 개선점을 확인할 수 있어요! 현직 개발자이자 채용 담당자가 만든 AI 분석으로 더욱 정확한 피드백을 받아보세요 📝


profile
기부하면 코드 드려요

9개의 댓글

comment-user-thumbnail
2025년 7월 28일

SSGOI 너무 실용적인 오픈소스인 거 같습니다! 👍👍👍

1개의 답글
comment-user-thumbnail
2025년 7월 30일

좋은 정보 감사합니다. 다음 프로젝트에 꼭 적용해볼게요.

1개의 답글
comment-user-thumbnail
2025년 8월 1일

すごい !

답글 달기
comment-user-thumbnail
2025년 8월 1일

좋은 글 잘 읽고 갑니다.

답글 달기
comment-user-thumbnail
2025년 8월 3일

Unlimited music Spotify Mod https://spotimodi.com/ offers users unrestricted access to millions of songs with premium features like offline mode, high-quality audio, and no ads. With Unlimited music Spotify Mod, enjoy nonstop streaming without limitations on your Android device.

답글 달기
comment-user-thumbnail
2025년 8월 4일

The Enneagram Personality Test on EnneagramTypes101.com is a quick, insightful tool designed to help you uncover your core type, motivations, and emotional patterns.
https://enneagramtypes101.com/

답글 달기

Thanks for sharing. Let me introduce you to Grow A Garden Generator, a super healing interactive website. With just a few clicks, you can create a colorful, unique digital garden. Each plant is unique, perfect for your profile picture, wallpaper, or simply gifting it to a friend as a "digital plant pet." 💐
📎 Try it now:
👉 https://growagardengenerator.xyz/
Add some green healing to your daily life! 💚

답글 달기