Nuxt로 네이티브 앱 같은 웹앱 만들기
웹으로 앱을 만들고 싶다면?
웹 기술로 앱을 만들 수 있다는 거 알고 계셨나요? Nuxt로 웹앱을 만들고, Expo나 Flutter의 웹뷰로 감싸면 바로 앱스토어에 출시할 수 있습니다.
하지만 웹앱의 가장 큰 약점이 있습니다. 바로 페이지 전환이 어색하다는 것.
네이티브 앱은 화면 전환이 자연스럽습니다. 리스트에서 상세로 들어갈 때 슬라이드되고, 이미지를 탭하면 확대되면서 열리죠. 하지만 웹은? 페이지가 그냥 뚝 바뀝니다.
만약 웹에서도 네이티브 같은 페이지 전환이 가능하다면? 그 웹앱은 진짜 앱처럼 느껴질 겁니다.
오늘은 Nuxt로 네이티브 앱 같은 페이지 트랜지션이 들어간 웹앱을 만들어봅니다.
SSGOI란?
SSGOI(쓱오이)는 모든 브라우저에서 동작하는 페이지 트랜지션 라이브러리입니다.
Chrome의 View Transition API가 있지만, Safari와 Firefox에서는 동작하지 않습니다. SSGOI는 이 문제를 해결합니다.
SSGOI의 장점:
- 모든 모던 브라우저 지원 (Chrome, Firefox, Safari, Edge)
- SSR/SSG 완벽 호환
- 스프링 기반 물리 애니메이션으로 자연스러운 움직임
- 다양한 빌트인 트랜지션 (Drill, Slide, Pinterest, Instagram 등)
시작하기
SSGOI 공식 템플릿으로 시작해봅시다.
# 레포지토리 클론
git clone https://github.com/meursyphus/ssgoi
# 루트에서 의존성 설치 (중요!)
pnpm install
# Nuxt 템플릿 폴더로 이동
cd templates/nuxt
# 개발 서버 실행
pnpm run dev
주의:
pnpm install은 반드시 루트 경로에서 실행해야 합니다. 모노레포 구조라서 루트에서 전체 의존성을 설치합니다.
브라우저에서 http://localhost:3000을 열면 템플릿 데모를 확인할 수 있습니다.
폴더 구조 살펴보기
templates/nuxt/ 폴더 구조입니다.
templates/nuxt/
├── app.vue # 루트 앱 컴포넌트
├── pages/
│ ├── posts/ # Drill 트랜지션 예제
│ │ ├── index.vue
│ │ └── [id].vue
│ ├── pinterest/ # Pinterest 트랜지션 예제
│ │ ├── index.vue
│ │ └── [id].vue
│ ├── profile/ # Instagram 트랜지션 예제
│ │ ├── index.vue
│ │ └── [id].vue
│ └── products.vue # Slide 트랜지션 예제
│ ├── all.vue
│ ├── electronics.vue
│ ├── fashion.vue
│ ├── home.vue
│ └── beauty.vue
├── components/
│ ├── demo-layout.vue # Ssgoi 설정 (핵심!)
│ ├── demo-wrapper.vue # 모바일 프레임 UI
│ ├── pin-card.vue
│ ├── post-card.vue
│ ├── profile-feed.vue
│ └── products/
└── composables/ # 데이터 관리
├── use-posts.ts
├── use-pinterest.ts
├── use-profile.ts
└── use-products.ts
핵심 파일은 components/demo-layout.vue입니다. 모든 트랜지션 설정이 여기에 있습니다.
Ssgoi 설정하기
components/demo-layout.vue를 열어보세요.
<template>
<main>
<Ssgoi :config="config">
<slot />
</Ssgoi>
</main>
</template>
<script setup lang="ts">
import { Ssgoi } from '@ssgoi/vue';
import type { SsgoiConfig } from '@ssgoi/vue';
import {
drill,
pinterest,
instagram,
} from '@ssgoi/vue/view-transitions';
const config: SsgoiConfig = {
transitions: [
// Pinterest 트랜지션
{
from: '/pinterest/*',
to: '/pinterest',
transition: pinterest(),
symmetric: true,
},
// Posts - Drill 트랜지션
{
from: '/posts',
to: '/posts/*',
transition: drill({ direction: 'enter' }),
},
{
from: '/posts/*',
to: '/posts',
transition: drill({ direction: 'exit' }),
},
// Profile - Instagram 트랜지션
{
from: '/profile',
to: '/profile/*',
transition: instagram(),
symmetric: true,
},
],
};
</script>
config 구성:
from: 출발 경로 (*는 와일드카드)to: 도착 경로transition: 사용할 트랜지션symmetric: 역방향도 같은 트랜지션 적용
SsgoiTransition으로 페이지 감싸기
각 페이지는 SsgoiTransition으로 감싸고 고유한 id를 부여해야 합니다.
pages/posts/index.vue를 보면:
<template>
<SsgoiTransition id="/posts">
<div class="min-h-full bg-[#121212] px-4 py-6">
<!-- 포스트 목록 -->
<div class="space-y-2">
<NuxtLink
v-for="post in posts"
:key="post.id"
:to="`/posts/${post.id}`"
>
<!-- 포스트 카드 -->
</NuxtLink>
</div>
</div>
</SsgoiTransition>
</template>
<script setup lang="ts">
import { SsgoiTransition } from '@ssgoi/vue';
const posts = useAllPosts();
</script>
pages/posts/[id].vue를 보면:
<template>
<SsgoiTransition :id="`/posts/${id}`">
<div class="min-h-screen bg-[#121212]">
<!-- 포스트 상세 -->
</div>
</SsgoiTransition>
</template>
<script setup lang="ts">
import { SsgoiTransition } from '@ssgoi/vue';
const route = useRoute();
const id = route.params.id as string;
const post = usePost(id);
</script>
핵심:
id는 config의from/to와 매칭되어야 SSGOI가 트랜지션을 적용합니다.
Drill 트랜지션
리스트에서 상세로 들어가는 느낌의 트랜지션입니다.

템플릿 위치: pages/posts/
import { drill } from '@ssgoi/vue/view-transitions';
// 리스트 → 상세 (들어갈 때)
drill({ direction: 'enter' });
// 상세 → 리스트 (나올 때)
drill({ direction: 'exit' });
config 설정:
{
from: '/posts',
to: '/posts/*',
transition: drill({ direction: 'enter' }),
},
{
from: '/posts/*',
to: '/posts',
transition: drill({ direction: 'exit' }),
},
옵션:
direction:"enter"|"exit"opacity:true면 페이드 효과 추가
Slide 트랜지션
탭 UI에서 좌우로 슬라이드하는 효과입니다.

템플릿 위치: pages/products.vue
import { slide } from '@ssgoi/vue/view-transitions';
// 왼쪽으로 슬라이드
slide({ direction: 'left' });
// 오른쪽으로 슬라이드
slide({ direction: 'right' });
pages/products.vue에서는 중첩된 Ssgoi를 사용해 탭 영역만 트랜지션합니다:
<template>
<SsgoiTransition id="/products">
<!-- 헤더, 탭 버튼 -->
<div class="flex-1 overflow-hidden">
<Ssgoi :config="config">
<NuxtPage />
</Ssgoi>
</div>
</SsgoiTransition>
</template>
<script setup lang="ts">
import { Ssgoi, SsgoiTransition } from '@ssgoi/vue';
import type { SsgoiConfig } from '@ssgoi/vue';
import { slide } from '@ssgoi/vue/view-transitions';
const categories = [
{ id: 'all', label: 'All', path: '/products/all' },
{ id: 'electronics', label: 'Tech', path: '/products/electronics' },
{ id: 'fashion', label: 'Fashion', path: '/products/fashion' },
{ id: 'home', label: 'Home', path: '/products/home' },
{ id: 'beauty', label: 'Beauty', path: '/products/beauty' },
];
const config: SsgoiConfig = {
transitions: [
{
from: '/products/tab/left',
to: '/products/tab/right',
transition: slide({ direction: 'left' }),
},
{
from: '/products/tab/right',
to: '/products/tab/left',
transition: slide({ direction: 'right' }),
},
],
middleware: (from: string, to: string) => {
const fromIndex = categories.findIndex((c) => c.path === from);
const toIndex = categories.findIndex((c) => c.path === to);
if (fromIndex !== -1 && toIndex !== -1 && fromIndex !== toIndex) {
if (fromIndex < toIndex) {
return { from: '/products/tab/left', to: '/products/tab/right' };
} else {
return { from: '/products/tab/right', to: '/products/tab/left' };
}
}
return { from, to };
},
};
</script>
Pinterest 트랜지션
갤러리 이미지가 확대되면서 상세로 전환되는 효과입니다.

템플릿 위치: pages/pinterest/
Data 어트리뷰트 설정 (필수!)
Pinterest 트랜지션은 data 어트리뷰트로 어떤 이미지끼리 연결되는지 지정해야 합니다.
components/pin-card.vue (갤러리):
<template>
<NuxtLink :to="`/pinterest/${item.id}`">
<div class="relative" :style="{ aspectRatio: item.aspectRatio }">
<img
:src="item.image"
:alt="item.title"
class="w-full h-full object-cover"
:data-pinterest-gallery-key="item.id"
/>
</div>
</NuxtLink>
</template>
<script setup lang="ts">
defineProps<{
item: PinterestItem;
}>();
</script>
pages/pinterest/[id].vue (상세):
<template>
<SsgoiTransition :id="`/pinterest/${id}`">
<div v-if="item">
<img
:src="item.image"
:alt="item.title"
:style="{ aspectRatio: item.aspectRatio }"
:data-pinterest-detail-key="item.id"
/>
</div>
</SsgoiTransition>
</template>
<script setup lang="ts">
import { SsgoiTransition } from '@ssgoi/vue';
const route = useRoute();
const id = route.params.id as string;
const item = usePinterestItem(id);
</script>
핵심:
data-pinterest-gallery-key와data-pinterest-detail-key에 같은 id를 넣어야 SSGOI가 연결을 인식합니다.
config 설정:
{
from: '/pinterest/*',
to: '/pinterest',
transition: pinterest(),
symmetric: true,
}
Instagram 트랜지션
프로필 피드 그리드에서 상세로 전환되는 효과입니다. Pinterest와 비슷하지만, 갤러리가 그대로 유지됩니다.

템플릿 위치: pages/profile/
Data 어트리뷰트 설정
components/post-card.vue (그리드):
<template>
<NuxtLink :to="`/profile/${post.id}`">
<img
:src="post.coverImage.url"
:alt="post.title"
class="w-full h-auto object-cover"
:data-instagram-gallery-key="post.id"
/>
</NuxtLink>
</template>
<script setup lang="ts">
defineProps<{
post: ProfilePost;
}>();
</script>
pages/profile/[id].vue (상세):
<template>
<SsgoiTransition :id="`/profile/${id}`">
<div v-if="post">
<img
:src="post.coverImage.url"
:alt="post.title"
:data-instagram-detail-key="post.id"
/>
</div>
</SsgoiTransition>
</template>
<script setup lang="ts">
import { SsgoiTransition } from '@ssgoi/vue';
const route = useRoute();
const id = route.params.id as string;
const post = useProfilePost(id);
</script>
config 설정:
{
from: '/profile',
to: '/profile/*',
transition: instagram(),
symmetric: true,
}
Spring 옵션으로 타이밍 조절하기
모든 트랜지션은 스프링 기반 물리 애니메이션을 사용합니다. spring 옵션으로 속도와 느낌을 조절할 수 있습니다.
drill({
direction: 'enter',
spring: {
stiffness: 200, // 높을수록 빠름
damping: 20, // 높을수록 덜 튕김
doubleSpring: true, // ease-in-out 효과 켜기
},
});
Spring 옵션 설명
| 옵션 | 설명 | 효과 |
|---|---|---|
stiffness | 강성 | 높을수록 빠르고 즉각적 |
damping | 감쇠 | 높을수록 덜 튕기고 부드러움 |
doubleSpring | 더블 스프링 | true/false - ease-in-out 효과 |
doubleSpring이란? CSS의 ease-in-out처럼 시작과 끝이 부드러운 효과입니다. 자세한 내용은 Double Spring 블로그 글을 참고하세요.
유용한 옵션들
symmetric 옵션
양방향 트랜지션을 자동 설정합니다.
// 이렇게 하나만 설정하면
{
from: '/pinterest/*',
to: '/pinterest',
transition: pinterest(),
symmetric: true,
}
// 역방향도 자동 적용됩니다
와일드카드 라우트
*로 모든 하위 경로를 매칭합니다.
// /posts/1, /posts/abc 모두 매칭
{ from: '/posts', to: '/posts/*', ... }
마무리
이제 여러분도 Nuxt 웹앱에 네이티브 같은 페이지 트랜지션을 적용할 수 있습니다!
리소스
- GitHub: https://github.com/meursyphus/ssgoi
- 공식 문서: https://ssgoi.dev
- Nuxt 템플릿: https://github.com/meursyphus/ssgoi/tree/main/templates/nuxt
웹뷰로 감싸서 진짜 앱으로 만들어보세요!