핀터레스트 전환

카드가 화면을 가득 채우며 확장되는 몰입형 전환

핀터레스트 전환

핀터레스트 전환은 갤러리의 작은 아이템이 전체 화면으로 확장되는 **확장형 전환(Expansion Transition)**입니다. 사용자가 선택한 콘텐츠가 화면을 가득 채우며 확대되어, 강력한 몰입감과 시각적 연속성을 제공합니다.

데모

Loading demo...

UX 원칙

언제 사용하나요?

핀터레스트 전환은 시각적 콘텐츠의 확대 탐색 시 사용됩니다.

콘텐츠 관계별 적합성

콘텐츠 관계적합성설명
관련 없는 콘텐츠확장할 연결점이 없어 부적합
형제 관계⚠️갤러리 내 아이템 간 이동 시 고려 가능
계층 관계썸네일→풀뷰처럼 같은 콘텐츠의 확대 뷰에 최적

주요 사용 케이스

  • 이미지 갤러리: 썸네일에서 고해상도 이미지로 확대
  • 제품 카탈로그: 제품 그리드에서 상세 정보로 확장
  • 미디어 브라우징: 비디오 썸네일에서 플레이어로 전환
  • 포트폴리오: 작품 목록에서 전체 화면 프레젠테이션으로

왜 이렇게 동작하나요?

  1. 공간 확장 메타포: 작은 창문을 통해 큰 세계로 들어가는 느낌
  2. 자연스러운 초점 이동: 사용자의 시선이 자연스럽게 확장되는 요소를 따라감
  3. 컨텍스트 보존: 어디서 왔는지 명확하여 방향감각 유지
  4. 드라마틱한 효과: 확장 애니메이션이 콘텐츠의 중요성 강조

모션 디자인

확장 과정:
1. 선택된 카드의 위치와 크기 캡처
2. 카드가 화면 중앙으로 이동하며 확대
3. 동시에 배경 페이드와 스케일 변화
4. 최종적으로 전체 화면 채움

축소 과정:
1. 상세 화면이 축소되며 원래 위치로
2. 배경이 다시 나타나며 갤러리 복원
3. 자연스러운 스프링 애니메이션으로 정착

기본 사용법

1. 전환 설정

import { Ssgoi } from '@ssgoi/react';
import { pinterest } from '@ssgoi/react/view-transitions';

const config = {
  transitions: [
    {
      from: '/gallery',
      to: '/gallery/*',
      transition: pinterest(),
      symmetric: true
    }
  ]
};

export default function App() {
  return (
    <Ssgoi config={config}>
      {/* 앱 내용 */}
    </Ssgoi>
  );
}

2. 요소 마킹

갤러리와 상세 페이지에 각각 다른 data 속성 사용:

// 갤러리 페이지
function Gallery() {
  return (
    <div className="masonry-grid">
      {items.map(item => (
        <Link 
          key={item.id} 
          to={`/gallery/${item.id}`}
          data-pinterest-gallery-key={item.id}
          className="gallery-item"
        >
          <img src={item.image} alt={item.title} />
          <h3>{item.title}</h3>
        </Link>
      ))}
    </div>
  );
}

// 상세 페이지
function ItemDetail({ item }) {
  return (
    <div 
      data-pinterest-detail-key={item.id}
      className="detail-container"
    >
      <button onClick={() => navigate('/gallery')}>
        ✕ Close
      </button>
      <img src={item.image} alt={item.title} />
      <article>
        <h1>{item.title}</h1>
        <p>{item.description}</p>
      </article>
    </div>
  );
}

실전 활용 예시

1. 메이슨리 갤러리

Pinterest 스타일 메이슨리 레이아웃:

// 메이슨리 그리드
function MasonryGallery() {
  return (
    <div className="columns-2 md:columns-3 lg:columns-4 gap-4">
      {images.map((img, index) => (
        <div
          key={img.id}
          data-pinterest-gallery-key={img.id}
          onClick={() => navigate(`/image/${img.id}`)}
          className="break-inside-avoid mb-4 cursor-pointer 
                     hover:scale-105 transition-transform"
        >
          <img 
            src={img.url} 
            alt={img.title}
            className="w-full rounded-lg"
          />
          <div className="p-2">
            <p className="text-sm font-medium">{img.title}</p>
            <p className="text-xs text-gray-500">{img.author}</p>
          </div>
        </div>
      ))}
    </div>
  );
}

// 확장된 뷰
function ImageView({ image }) {
  return (
    <div 
      data-pinterest-detail-key={image.id}
      className="fixed inset-0 bg-white z-50 overflow-auto"
    >
      <div className="max-w-4xl mx-auto p-4">
        <img 
          src={image.url} 
          alt={image.title}
          className="w-full rounded-lg shadow-xl"
        />
        <div className="mt-4">
          <h1 className="text-2xl font-bold">{image.title}</h1>
          <p className="text-gray-600 mt-2">{image.description}</p>
        </div>
      </div>
    </div>
  );
}

2. 제품 카탈로그

제품 카드가 상세 페이지로 확장:

// 제품 그리드
function ProductGrid() {
  return (
    <div className="grid grid-cols-2 md:grid-cols-4 gap-6">
      {products.map(product => (
        <article
          key={product.id}
          data-pinterest-gallery-key={product.id}
          onClick={() => navigate(`/product/${product.id}`)}
          className="bg-white rounded-xl shadow-lg overflow-hidden
                     cursor-pointer hover:shadow-xl transition-shadow"
        >
          <img 
            src={product.image} 
            alt={product.name}
            className="w-full h-48 object-cover"
          />
          <div className="p-4">
            <h3 className="font-semibold">{product.name}</h3>
            <p className="text-lg font-bold mt-2">${product.price}</p>
          </div>
        </article>
      ))}
    </div>
  );
}

// 제품 상세
function ProductDetail({ product }) {
  return (
    <div 
      data-pinterest-detail-key={product.id}
      className="min-h-screen bg-white"
    >
      <div className="grid md:grid-cols-2 gap-8 p-8">
        <img 
          src={product.image} 
          alt={product.name}
          className="w-full rounded-lg"
        />
        <div>
          <h1 className="text-3xl font-bold">{product.name}</h1>
          <p className="text-2xl font-bold mt-4">${product.price}</p>
          <p className="mt-4 text-gray-600">{product.description}</p>
          <button className="mt-6 bg-black text-white px-8 py-3 rounded-lg">
            Add to Cart
          </button>
        </div>
      </div>
    </div>
  );
}

커스터마이징

스프링 설정

애니메이션의 탄성과 속도 조절:

pinterest({
  spring: {
    stiffness: 200,  // 낮을수록 부드러움
    damping: 25      // 높을수록 빠른 정착
  }
})

주의사항

키 속성 구분

  • 갤러리: data-pinterest-gallery-key
  • 상세: data-pinterest-detail-key
  • 반드시 다른 속성명 사용 (히어로와 차별화)

레이아웃 고려사항

  • 갤러리 아이템은 position이 static이어야 함
  • 상세 페이지는 충분한 여백 필요
  • 메이슨리 레이아웃과 특히 잘 어울림

접근성

  • ESC 키로 상세 페이지 닫기 지원
  • 포커스 트랩으로 키보드 네비게이션 개선
  • 모션 감소 설정 시 즉시 전환

모범 사례

✅ DO

  • 이미지 중심 콘텐츠에 사용
  • 갤러리나 카탈로그 형태에 적용
  • 닫기 버튼을 명확하게 제공
  • 모바일에서도 잘 작동하도록 반응형 디자인

❌ DON'T

  • 텍스트 중심 콘텐츠에는 부적합
  • 너무 많은 요소가 동시에 확장되지 않도록
  • 상세 페이지 로딩이 느린 경우 사용 자제
  • 복잡한 레이아웃에서는 예상치 못한 동작 가능