인스타그램 전환
이미지가 확장되며 상세 화면으로 전환되는 인스타그램 스타일 전환
인스타그램 전환
인스타그램 전환은 그리드의 작은 이미지가 전체 화면으로 확장되는 **확장형 전환(Expansion Transition)**입니다. 핀터레스트 전환과 유사하지만, 나가는 페이지는 애니메이션 없이 그대로 유지되고 들어오는 페이지만 애니메이션됩니다.
데모
Loading demo...
UX 원칙
언제 사용하나요?
인스타그램 전환은 이미지 중심 피드에서 상세 보기로의 전환 시 사용됩니다.
콘텐츠 관계별 적합성
| 콘텐츠 관계 | 적합성 | 설명 |
|---|---|---|
| 관련 없는 콘텐츠 | ❌ | 확장할 연결점이 없어 부적합 |
| 형제 관계 | ⚠️ | 그리드 내 이미지 간 이동 시 고려 가능 |
| 계층 관계 | ✅ | 썸네일→풀뷰처럼 같은 콘텐츠의 확대 뷰에 최적 |
주요 사용 케이스
- 소셜 미디어 피드: 그리드 피드에서 포스트 상세로 전환
- 사진 갤러리: 작은 썸네일에서 큰 이미지로 확대
- 포트폴리오: 작품 그리드에서 상세 뷰로
- 제품 목록: 제품 그리드에서 상세 페이지로
왜 이렇게 동작하나요?
- 집중 유지: 나가는 페이지가 그대로 유지되어 사용자가 어디서 왔는지 명확함
- 성능 최적화: 한 페이지만 애니메이션하여 부드러운 전환 보장
- 자연스러운 확장: 선택한 이미지가 자연스럽게 화면을 채움
- 컨텍스트 보존: 배경이 유지되어 방향감각 상실 방지
모션 디자인
확장 과정 (Gallery → Detail):
1. 갤러리 페이지는 그대로 유지 (애니메이션 없음)
2. 상세 페이지가 선택된 이미지 위치에서 확장
3. clip-path로 화면을 채우며 확대
4. 최종적으로 전체 화면을 차지
축소 과정 (Detail → Gallery):
1. 상세 페이지는 그대로 유지 (애니메이션 없음)
2. 갤러리 페이지가 타겟 이미지 위치로 축소되며 나타남
3. 투명도와 스케일 애니메이션으로 부드럽게 등장
기본 사용법
1. 전환 설정
import { Ssgoi } from '@ssgoi/react';
import { instagram } from '@ssgoi/react/view-transitions';
const config = {
transitions: [
{
from: '/gallery',
to: '/gallery/*',
transition: instagram(),
symmetric: true
}
]
};
export default function App() {
return (
<Ssgoi config={config}>
{/* 앱 내용 */}
</Ssgoi>
);
}
2. 요소 마킹
갤러리와 상세 페이지에 각각 다른 data 속성 사용:
// 갤러리 페이지 (3열 그리드)
function Gallery() {
return (
<div className="grid grid-cols-3 gap-1">
{items.map(item => (
<Link
key={item.id}
to={`/gallery/${item.id}`}
className="aspect-square"
>
<img
src={item.image}
alt={item.title}
data-instagram-gallery-key={item.id}
className="w-full h-full object-cover"
/>
</Link>
))}
</div>
);
}
// 상세 페이지
function PostDetail({ item }) {
return (
<div className="min-h-screen">
<img
src={item.image}
alt={item.title}
data-instagram-detail-key={item.id}
className="w-full h-auto"
/>
<div className="p-4">
<p className="font-semibold">{item.likes} likes</p>
<p className="mt-2">{item.caption}</p>
</div>
</div>
);
}
실전 활용 예시
1. 인스타그램 스타일 피드
균일한 3열 그리드:
// 3열 그리드
function InstagramFeed() {
return (
<div className="grid grid-cols-3 gap-1">
{posts.map((post) => (
<div
key={post.id}
onClick={() => navigate(`/post/${post.id}`)}
className="relative aspect-square cursor-pointer group"
>
<img
src={post.image}
alt={post.title}
data-instagram-gallery-key={post.id}
className="w-full h-full object-cover"
/>
{/* Hover overlay */}
<div className="absolute inset-0 bg-black/40 opacity-0
group-hover:opacity-100 transition-opacity
flex items-center justify-center">
<span className="text-white font-semibold">
❤️ {post.likes.toLocaleString()}
</span>
</div>
</div>
))}
</div>
);
}
// 포스트 상세
function PostView({ post }) {
return (
<div className="min-h-screen bg-black">
<img
src={post.image}
alt={post.title}
data-instagram-detail-key={post.id}
className="w-full h-auto"
/>
{/* Post details */}
<div className="bg-white p-4">
<div className="flex items-center gap-4 mb-4">
<button className="flex items-center gap-2">
<HeartIcon className="w-6 h-6" />
<span>{post.likes}</span>
</button>
<button>
<CommentIcon className="w-6 h-6" />
</button>
<button>
<ShareIcon className="w-6 h-6" />
</button>
</div>
<p className="text-sm">
<strong>{post.author}</strong> {post.caption}
</p>
<p className="text-xs text-gray-500 mt-2">
{post.timestamp}
</p>
</div>
</div>
);
}
2. 사진 포트폴리오
작품 그리드에서 풀 스크린으로:
// 포트폴리오 그리드
function PortfolioGrid() {
return (
<main className="container mx-auto p-4">
<div className="grid grid-cols-3 gap-4">
{works.map((work) => (
<article
key={work.id}
onClick={() => navigate(`/work/${work.id}`)}
className="relative aspect-square rounded-lg overflow-hidden
cursor-pointer hover:scale-105 transition-transform"
>
<img
src={work.thumbnail}
alt={work.title}
data-instagram-gallery-key={work.id}
className="w-full h-full object-cover"
/>
<div className="absolute bottom-0 left-0 right-0 p-3
bg-gradient-to-t from-black/80 to-transparent">
<h3 className="text-white font-semibold text-sm">
{work.title}
</h3>
<p className="text-white/80 text-xs">{work.category}</p>
</div>
</article>
))}
</div>
</main>
);
}
// 작품 상세
function WorkDetail({ work }) {
return (
<div className="min-h-screen bg-gray-900">
<div className="max-w-4xl mx-auto">
<img
src={work.image}
alt={work.title}
data-instagram-detail-key={work.id}
className="w-full h-auto"
/>
<div className="p-6 text-white">
<h1 className="text-3xl font-bold mb-2">{work.title}</h1>
<p className="text-gray-400 mb-4">{work.category}</p>
<p className="text-gray-300 leading-relaxed">
{work.description}
</p>
<div className="mt-6 flex gap-4">
<span className="text-sm text-gray-400">
{work.date}
</span>
<span className="text-sm text-gray-400">
{work.client}
</span>
</div>
</div>
</div>
</div>
);
}
커스터마이징
스프링 설정
애니메이션의 탄성과 속도 조절:
instagram({
spring: {
stiffness: 150, // 낮을수록 부드러움
damping: 20 // 높을수록 빠른 정착
}
})
주의사항
키 속성 구분
- 갤러리:
data-instagram-gallery-key - 상세:
data-instagram-detail-key - 반드시 다른 속성명 사용
레이아웃 고려사항
- 갤러리는 균일한 그리드 (3열 권장)
- 상세 페이지는 충분한 여백 필요
- 이미지는 aspect-ratio 유지
핀터레스트 전환과의 차이점
- 핀터레스트: 양쪽 페이지 모두 애니메이션
- 인스타그램: 들어오는 페이지만 애니메이션
- 성능: 인스타그램이 더 가볍고 빠름
- 용도: 인스타그램은 균일한 그리드에 최적
접근성
- ESC 키로 상세 페이지 닫기 지원
- 키보드 네비게이션 개선
- 모션 감소 설정 시 즉시 전환
모범 사례
✅ DO
- 균일한 3열 그리드에 사용
- 이미지 중심 콘텐츠에 적용
- 닫기 버튼을 명확하게 제공
- 모바일 최적화된 레이아웃
❌ DON'T
- 불규칙한 그리드에는 부적합
- 텍스트 중심 콘텐츠에는 사용 자제
- 너무 많은 요소가 동시에 확장되지 않도록
- 느린 네트워크에서 대용량 이미지 사용 주의