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

Dan Hollick

우리는 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에서 재생 상태를 추적할 필요가 없습니다. 부모가 hover될 때 애니메이션을 시작하기 위해 group-hover:[animation-play-state:running] 클래스를 사용할 수 있습니다.

아마 눈치채셨겠지만, 이 컴포넌트에서는 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를 사용해 보기로 결정했습니다. 많은 긍정적인 평가를 받았기 때문입니다.

파일을 생성하고 커밋을 만들거나 이미지를 직접 관리하는 대신, 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="mt-12 mb-10 text-2xl/8 font-medium tracking-tight text-gray-950 first:mt-0 last:mb-0">          {children}        </h2>      ),      h3: ({ children }) => (        <h3 className="mt-12 mb-10 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 작업을 지원하고 앞으로도 멋진 것들을 계속 만들어 나갈 수 있게 해주는 가장 좋은 방법입니다.

모든 업데이트를 직접 받아 볼 수 있습니다.
뉴스레터에 가입하세요.