SPA 프로젝트에 Next.js를 가미하여 오픈그래프 기능 구현하기

2023-12-24

저희 셀럽잇 팀은 Sing Page Application으로 서비스를 개발하고 있습니다. 그렇다보니 SPA의 치명적인 단점인 SEO 최적화 문제에 직면하게 되었습니다. 그럼에도 불구하고 셀럽들이 다녀간 맛집에 대한 상세페이지를 유저간 공유하는 과정에서 오픈그래프가 노출되도록 하고 싶었고 여러 시도 끝에 이를 해결한 방법을 소개해보고자합니다.

왜 SPA에서는 검색엔진최적화(SEO)가 어려울까?

SPA는 html 문서위에서 자바스크립트가 실행되며 사용자와의 상호작용에 따라 렌더링이 발생합니다. 즉 페이지를 이동할 때 서버로부터 새로운 html문서를 받아오는게 아닌 자바스크립트 코드가 실행되며 페이지가 이동하는 것처럼 보이는 것이죠. 그래서 SPA는 단일 메타데이터를 소유할 수 밖에 없습니다.

동적으로 메타데이터를 삽입하거나 수정하면?

난황을 겪던 중 '그럼 동적으로 메타데이터를 업데이트하면 오픈그래프가 노출되지 않을까?'라는 아이디어가 떠올랐습니다. 그래서 이를 가능하게 해주는 라이브러리를 탐색하였고, react-helmet 라는 동적으로 메타데이터를 업데이트 시켜주는 라이브러리를 발견하게 되었습니다.

결과는 어떻게 되었을까요?

절반의 성공을 거두었습니다. 크롬과 같은 특정 브라우저에서는 오픈그래프가 노출되었지만, 저희의 타겟인 카카오톡에서는 여전히 노출되지 않았습니다.

그 이유는 무엇일까요?

오픈그래프가 노출되는 동작원리에 대해서 짧게 설명을 드리자면, 각 브라우저마다 각기다른 크롤링 봇을 지니고 있습니다. 이 크롤링 봇은 서버로부터 받은 html 문서만을 크롤링하기도하고, 자바스크립트 코드를 실행시켜 렌더링 된 문서를 크롤링하기도 합니다. 크롬의 크롤링 봇은 자바스크립트 코드까지 실행시켜 크롤링을 진행하기에 오픈그래프를 노출 시킬 수 있었던 거죠.

Next.js를 부분적으로 도입

렌더링 방식의 차이를 공부하며 Next.js로 클론코딩을 진행했었는데, 이 때 generateMetadata라는 기능을 제공하는 것을 알게 되었습니다. 여기서 아이디어를 착안했죠.

'공유하기를 클릭하면 맵핑된 Next.js 주소를 복사시키고, 이 페이지에 접속하면 실제 서비스 주소로 리다이렉트시켜주면 되겠구나?'

import { getRestaurantDetail } from '@/api/restaurant';
import Redirection from '@/components/client/Redirection';
import { CELEB } from '@/constants/celeb';
import { Metadata } from 'next';

interface Props {
  params: { restaurantId: string; celebId: string };
}

export async function generateMetadata({
  params: { restaurantId, celebId },
}: Props): Promise<Metadata> {
  const restaurant = await getRestaurantDetail({
    restaurantId: Number(restaurantId),
    celebId: Number(celebId),
  });

  return {
    openGraph: {
      title: restaurant.name,
      description: `${
        CELEB[Number(celebId) as keyof typeof CELEB]
      }이(가) 다녀간 맛집 ${restaurant.name}을 셀럽잇에서 확인해보세요.`,
      images: [
        `${이미지 저장소}/${restaurant.images[0].name}.webp`,
      ],
    },
  };
}

const RestaurantPage = ({ params: { restaurantId, celebId } }: Props) => {
  return (
    <div>
      <Redirection restaurantId={restaurantId} celebId={celebId} />
    </div>
  );
};

export default RestaurantPage;

Vercel로 배포를 진행하고나서 QA 직전의 떨림이 아직도 기억이 나네요.

QA 결과

위 기능을 직접 체험해보고 싶으시다면 셀럽잇에서 확인가능합니다.