Radiant: 아름다운 새로운 마케팅 사이트 템플릿

Date

우리는 방금 Radiant라는 아름다운 새로운 SaaS 마케팅 사이트 템플릿 작업을 마쳤고, 이제 Tailwind UI의 일부로 제공됩니다.

Radiant 템플릿 알아보기

이 템플릿은 Next.js, Framer Motion, Tailwind CSS로 구축되었으며, Sanity로 구동되는 블로그를 포함하고 있습니다.

이런 종류의 SaaS 마케팅 템플릿을 만든 지 꽤 시간이 지났고, 그동안 우리는 이런 템플릿이 유용하고 작업하기 쉬운 이유에 대해 많은 것을 배웠습니다. 우리는 그 모든 학습을 Radiant에 반영하려고 노력했습니다.

항상 그렇듯이 라이브 미리보기를 확인해보세요. 이번 템플릿에는 브라우저에서 직접 확인해야만 제대로 감상할 수 있는 멋진 디테일이 많이 있습니다.


세련된 상호작용

이런 사이트에서 애니메이션을 과도하게 사용하기는 매우 쉽습니다. 우리 모두는 스크롤을 조금만 해도 수많은 요소들이 애니메이션으로 나타나는 사이트를 본 적이 있을 겁니다. 더 나쁜 것은 콘텐츠가 나타나기를 기다려야 하기 때문에 사이트가 느리게 느껴지는 경우입니다.

Radiant은 매력적인 애니메이션으로 가득 차 있지만, 모든 애니메이션은 기존 콘텐츠 위에 레이어로 추가되고 사용자 상호작용에 의해 트리거되기 때문에 사이트는 여전히 빠르게 느껴집니다. 대부분의 경우, 요소들이 상호작용 중에 “살아있는” 느낌을 주기 위해 반복되는 애니메이션을 선택했습니다.

우리는 거의 모든 애니메이션에 Framer Motion을 사용했습니다. 이 라이브러리는 선언적이기 때문에 복잡한 애니메이션을 위한 API를 쉽게 만들 수 있고, 다른 사람들이 쉽게 커스텀할 수 있습니다.

하지만 몇 가지 단점도 있습니다. 예를 들어, 여러 요소가 독립적으로 애니메이션될 때 각 자식 요소에 hover 상태를 전달하는 것이 번거로울 수 있습니다. 우리는 Framer의 variant 전파 기능을 활용하여 이 문제를 해결했습니다. hover 이벤트가 부모에서 variant 변경을 트리거하면, 자식 요소들이 같은 variant 키를 공유하기 때문에 이 변경이 자식들에게 전파됩니다.

bento-card.tsx
export function BentoCard() {
  return (
    <motion.div
      initial="idle"
      whileHover="active"
      variants={{ idle: {}, active: {} }}
      data-dark={dark ? 'true' : undefined}
    >
      /* ... */
    </motion.div>
  )
}

부모의 variant에는 차이가 없기 때문에 실제로는 변경되지 않지만, 자식 요소들은 hover 시 variant 변경 신호를 받습니다. 심지어 깊게 중첩된 경우에도 동일하게 작동합니다.

map.tsx
function Marker({
  src,
  top,
  offset,
  delay,
}: {
  src: string
  top: number
  offset: number
  delay: number
}) {
  return (
    <motion.div
      variants={{
        idle: { scale: 0, opacity: 0, rotateX: 0, rotate: 0, y: 0 },
        active: { y: [-20, 0, 4, 0], scale: [0.75, 1], opacity: [0, 1] },
      }}
      transition={{ duration: 0.25, delay, ease: 'easeOut' }}
      style={{ '--offset': `${offset}px`, top } as React.CSSProperties}
      className="absolute left-[calc(50%+var(--offset))] size-[38px] drop-shadow-[0_3px_1px_rgba(0,0,0,.15)]"
    >
      /* ... */
    </motion.div>
  )
}

/* ... */

로고 타임라인 애니메이션은 조금 다릅니다. 우리는 hover를 멈췄을 때 로고가 원래 위치로 돌아가는 대신 현재 위치에서 멈추기를 원했습니다. 이는 Framer의 시작 및 종료 상태 지정 방식과 잘 맞지 않아서 CSS로 구현하는 것이 더 쉬웠습니다.

이 방법은 animation-delay 값을 음수로 설정하여 요소의 시작 위치를 오프셋할 수 있다는 사실을 활용합니다. 이렇게 하면 모든 로고가 같은 애니메이션 키프레임을 공유하지만 다른 위치에서 시작하고 다른 지속 시간을 가질 수 있습니다.

logo-timeline.tsx
function Logo({
  label,
  src,
  className,
}: {
  label: string
  src: string
  className: string
}) {
  return (
    <div
      className={clsx(
        className,
        'absolute top-2 grid grid-cols-[1rem,1fr] items-center gap-2 whitespace-nowrap px-3 py-1',
        'rounded-full bg-gradient-to-t from-gray-800 from-50% to-gray-700 ring-1 ring-inset ring-white/10',
        '[--move-x-from:-100%] [--move-x-to:calc(100%+100cqw)] [animation-iteration-count:infinite] [animation-name:move-x] [animation-play-state:paused] [animation-timing-function:linear] group-hover:[animation-play-state:running]',
      )}
    >
      <img alt="" src={src} className="size-4" />
      <span className="text-sm/6 font-medium text-white">{label}</span>
    </div>
  )
}

export function LogoTimeline() {
  return (
    /* ... */
    <Row>
      <Logo
        label="Loom"
        src="./logo-timeline/loom.svg"
        className="[animation-delay:-26s] [animation-duration:30s]"
      />
      <Logo
        label="Gmail"
        src="./logo-timeline/gmail.svg"
        className="[animation-delay:-8s] [animation-duration:30s]"
      />
    </Row>
    /* ... */

이 방법을 사용하면 JavaScript에서 재생 상태를 추적할 필요가 없습니다. 대신 group-hover:[animation-play-state:running] 클래스를 사용하여 부모 요소에 hover 시 애니메이션을 시작할 수 있습니다.

아마 눈치채셨겠지만, 이 컴포넌트에서는 Tailwind에 없는 개별 animation 속성을 위해 여러 임의 속성을 사용하고 있습니다. 이런 템플릿을 만드는 것이 좋은 점은 Tailwind CSS의 빈틈을 찾을 수 있다는 것입니다. 누가 알겠어요, 아마 v4.0에서 이 유틸리티들이 추가될지도 모르죠!


의도적으로 재사용 가능하게 설계

이런 SaaS 템플릿을 디자인할 때 가장 까다로운 부분은, 사람들이 자신의 제품에 쉽게 적용할 수 있는 인터랙티브 요소를 만드는 것입니다. 템플릿을 구매했는데 예제 콘텐츠에 너무 특화되어 있어서 실제 프로젝트에 사용할 수 없는 상황보다 더 실망스러운 것은 없죠.

우리는 대부분의 SaaS 제품이 가질 만한 핵심 그래픽 요소를 고안했습니다. 핀이 있는 지도, 로고 클러스터, 키보드 등 다양한 기능에 적용할 수 있는 요소들입니다. 이 요소들을 여러분의 제품에 쉽게 재사용할 수 있도록, 많은 부분을 코드로 구현하고 사용하기 편리한 API를 디자인했습니다.

예를 들어, 로고 클러스터는 간단한 API를 제공하여 여러분의 로고를 전달하고, 위치와 호버 애니메이션을 조정할 수 있습니다.

<Logo
  src="./logo-cluster/dribbble.svg"
  left={285}
  top={20}
  hover={{ x: 4, y: -5, rotate: 6, delay: 0.3 }}
/>

키보드 단축키 섹션도 좋은 예입니다. 여러분의 단축키를 추가하는 것은 키 이름 배열을 Keyboard 컴포넌트에 전달하는 것만큼 간단합니다. 각 키가 컴포넌트이기 때문에 커스텀 키를 쉽게 추가하거나 레이아웃을 변경할 수 있습니다.

<Keyboard highlighted={['F', 'M', 'L']} />

코드로 키보드를 만드는 것은 상당히 많은 작업이 필요하지만, 적어도 이제 여러분은 직접 그런 고생을 하지 않아도 됩니다.

물론, 여러분의 제품 스크린샷을 넣을 수 있는 공간도 마련했습니다. SavvyCal 친구들을 위해 커스터마이징한 이 섹션은 동일한 인터랙티브 컴포넌트를 사용합니다.

Radiant as SavvyCal

CMS로 구동되는 블로그

보통 블로그를 템플릿에 추가할 때는 MDX를 사용하지만, 이번에는 헤드리스 CMS를 활용해 보는 것도 재미있을 것 같다는 생각이 들었습니다. 독자들에게 설문을 진행한 후, Sanity를 사용해 보기로 결정했습니다. 많은 사람들이 Sanity를 추천했기 때문입니다.

파일을 만들고, 커밋을 하고, 이미지를 직접 관리하는 대신, CMS를 사용하면 모든 작업을 UI에서 처리할 수 있습니다. 덕분에 개발자가 아닌 사람들도 쉽게 기여할 수 있습니다.

Sanity Studio

Sanity와 같은 헤드리스 CMS의 장점 중 하나는 콘텐츠를 구조화된 형식으로 받을 수 있다는 점입니다. MDX와 비슷하게, 각 엘리먼트를 커스텀 컴포넌트에 매핑하여 타이포그래피 스타일을 관리할 수 있습니다.

<PortableText
  value={post.body}
  components={{
    block: {
      normal: ({ children }) => (
        <p className="my-10 text-base/8 first:mt-0 last:mb-0">
          {children}
        </p>
      ),
      h2: ({ children }) => (
        <h2 className="mb-10 mt-12 text-2xl/8 font-medium tracking-tight text-gray-950 first:mt-0 last:mb-0">
          {children}
        </h2>
      ),
      h3: ({ children }) => (
        <h3 className="mb-10 mt-12 text-xl/8 font-medium tracking-tight text-gray-950 first:mt-0 last:mb-0">
          {children}
        </h3>
      ),
      blockquote: ({ children }) => (
        <blockquote className="my-10 border-l-2 border-l-gray-300 pl-6 text-base/8 text-gray-950 first:mt-0 last:mb-0">
          {children}
        </blockquote>
      ),
    },
    types: {
      image: ({ value }) => (
        <img
          className="w-full rounded-2xl"
          src={image(value).width(2000).url()}
          alt={value.alt || ''}
        />
      ),
    },
    /* ... */
  }}
/>

CMS를 사용하면 이미지와 같은 모든 에셋이 호스팅되며, 이미지의 크기, 품질, 포맷을 실시간으로 조절할 수 있습니다.

<div className="text-sm/5 max-sm:text-gray-700 sm:font-medium">
  {dayjs(post.publishedAt).format('dddd, MMMM D, YYYY')}
</div>
{post.author && (
  <div className="mt-2.5 flex items-center gap-3">
    {post.author.image && (
      <img
       className="aspect-square size-6 rounded-full object-cover"
       src={image(post.author.image).width(64).height(64).url()}
       alt=""
      />
    )}
    <div className="text-sm/5 text-gray-700">
      {post.author.name}
    </div>
  </div>
)}

마크다운의 프론트매터와 마찬가지로, 커스텀 필드를 추가하여 콘텐츠를 더 풍부하게 만들 수 있습니다. 예를 들어, 블로그 포스트 스키마에 featured 불리언 필드를 추가하여 특정 포스트를 블로그의 특별 섹션에서 강조할 수 있습니다.

Radiant Blog

Sanity는 유료 제품이지만, 꽤 관대한 무료 티어를 제공합니다. 이는 충분히 테스트해 보기에 적합합니다. 만약 다른 헤드리스 CMS를 시도하고 싶다면, 여기서 구현한 Sanity 통합이 다른 도구와 연결하는 방법에 대한 좋은 예시가 될 것입니다.


이것이 바로 Radiant입니다! 내부를 살펴보고, 테스트해 보시고, 의견을 알려주세요.

모든 템플릿과 마찬가지로, 이 템플릿은 Tailwind UI all-access 라이선스로 제공됩니다. 이 라이선스는 Tailwind CSS 개발을 지원하고, 앞으로도 멋진 기능을 계속 만들어 나갈 수 있게 해줍니다.