Next js 동적 SEO 구현기

박한영·2023년 1월 5일
7
post-thumbnail

SEO란?

SEO는 Search Engine Optimization의 약자로, Google과 같은 검색 엔진에서 상위 노출될 수 있도록 사이트를 최적화하는 일을 의미한다. 쉽게 말해 사용자가 특정 검색어를 쳤을 때, 우리 서비스가 최대한 상단으로 노출될 수 있도록 최적화하는 작업이다. 수많은 웹사이트를 돌아다니며 정보를 수집하는 웹 크롤러에게 적절한 정보를 제공함으로써 최적화를 이뤄낼 수 있다.

구현 배경

숙박 예약 서비스는 SEO 중요도가 높다. 예약할 숙소에 대한 검색 결과를 따라서 자연 유입되는 케이스가 많이 발생하기 때문이다.

그렇기 때문에 우리 서비스에서도 SEO가 중요했다. 특히 숙소 상세 페이지에 대해서 SEO를 구현하는 일이 중요했다. 처음엔 SEO가 단순히 Meta 태그를 통해 페이지에 대한 정보만 제공하면 끝나는 문제라고 생각하여 이 일이 쉽게 끝날 줄 알았다. 하지만 동적인 데이터를 포함하는 페이지의 경우, 기존 방식으로 구현할 수 없었다. 생각보다 오래 걸렸던 동적 SEO 구현 과정을, 직접 구현해나간 순서에 따라 기록하고자 한다.

>> HAU 숙소 상세페이지

동적 SEO 구현기

CSR(Client-Side-Rendering)

// Data Fetching
const [lodgment, setLodgment] = React.useState()

React.useEffect(() => {
  fetchLodgment();
}, [])


// Meta Tag(next-seo 사용)
<NextSeo
  title={lodgment.name}
  description={lodgment.description}
  canonical={`https://hautrip.com/lodgment/${lodgment.name}`}
  ...
/>

기존의 숙소 상세 페이지는 CSR 방식으로 구현되어 있었다. useEffect 내에서 데이터를 fetch하는 일반적인 방식이었다.

이런 식으로 렌더 이후에 받아 온 데이터는 Meta 태그를 통해 웹 크롤러에게 제공하는 것이 불가능했다. 왜냐하면 크롤러가 Meta 태그의 정보를 긁어가는 시점에는 해당 데이터가 로드되기 이전이어서 HTML이 모두 채워져 있지 않기 때문이다. CSR 방식에서 동적으로 받아온 정보는 표시되지 않았다.

SSR(Server-Side-Rendering)

function LodgmentDetailPage({ lodgment }) {
  ...
  return (
  <>
    <NextSeo
      title={lodgment.name}
      description={lodgment.description}
      canonical={`https://hautrip.com/lodgment/${lodgment.name}`}
      ...
    />
    ...
  </>
}

export async function getServerSideProps() {
  try {
    const res = await fetchLodgment();
    if (res.status !== 200) return { props: { lodgment: {} } };
    return { props: { lodgment: res.data } };
  } catch {
    return { props: { lodgment: {} } };
  }
}

*getServerSideProps: SSR을 구현하기 위한 함수로, 페이지 요청 시 실행되어 이 함수에서 반환된 props가 포함되어 페이지가 pre-render된다. 이 함수는 브라우저가 아닌 next 서버에서 돌아가는 함수이다. 페이지가 반환되는 시점에 즉시 필요한 정보들을 서버 사이드에서 미리 채워넣기 위해 활용된다.

CSR 방식으로 처리할 수 없었던 동적 SEO를 구현하기 위해 SSR을 선택했다. Next js의 getServerSideProps 메소드를 이용하여 SSR을 구현했다. SSR에서는 데이터 fetch를 서버 사이드에서 한 뒤, 동적 데이터를 포함하고 있는 정적 HTML을 전달받기 때문에 동적인 데이터까지 메타 태그에 포함시킬 수 있게 되었다. 쉽게 말해 클라이언트 서버로부터 이미 완성된(정보가 채워져 있는) HTML을 전달 받기 때문에 웹 크롤러가 모든 정보를 정상적으로 긁어갈 수 있게 된 것이다.

but...

숙소 상세 페이지에 들어가는 속도가 눈에 띄게 느려졌다. 첫 화면을 보게 되는 시간이 2~3초 정도로 지연되었다. 단순히 화면이 full render되기까지 오래 걸리게 된 것이 아니라, 페이지가 전환되는 시간이 지연된 것이라 특히 더 답답하게 느껴졌다. 페이지를 받아오기 이전, 서버 사이드에서 데이터 fetch 및 pre-render하는 데에 시간이 오래 걸렸기 때문이다.

SSG(Static-Site-Generation)

function LodgmentDetailPage({ lodgment }) {
  ...
  return (
  <>
    <NextSeo
      title={lodgment.name}
      description={lodgment.description}
      canonical={`https://hautrip.com/lodgment/${lodgment.name}`}
      ...
    />
    ...
  </>
}

export async function getStaticPaths() {
  const lodgmentIds = await getLodgmentIds();
  const paths = lodgmentIds.map(id => ({
    params: { id },
  }));
  return { paths, fallback: true };
}

export async function getStaticProps({ params }) {
  const lodgment = await getLodgmentById(params.id);
  return {
    props: {
      lodgment,
    },
  };
}

*getStaticPaths: "pages/~~/[id].tsx"와 같은 형식의 동적 라우팅 페이지 중, 빌드 시에 static하게 생성(pre-render)할 페이지를 지정하기 위해 사용되는 함수이다. 이 함수에서 반환된 객체의 paths는 getStaticProps의 인자로 전해진다. fallback은 paths로 리턴되지 않은 경로를 처리할 방식을 설정하는 옵션이다.
*getStaticProps: 빌드 시 실행되며, (동적 url일 경우) getStaticPaths로부터 전달받은 각 path에 관한 데이터를 fetch하여 pre-rendered 페이지를 만드는 함수이다. 이 함수에서 동적 데이터를 포함한 채 pre-render된 정적 페이지들을 서버에 쌓아두고, 유저가 요청할 시 즉시 서빙해줄 수 있다.

매 요청마다 서버에서 데이터를 fetch하는 SSR의 속도 문제를 개선하기 위해 SSG 방식을 선택했다. SSG는 빌드 시에 데이터를 fetch해서 미리 정적 HTML을 준비해두기 때문에 요청 시에는 네트워크 요청 및 pre-render로 인한 시간이 걸리지 않는다. 이 방식을 통해 동적 SEO를 구현함과 동시에 빠른 화면 전환 속도를 유지할 수 있었다.

but...

SSG를 통해 동적 SEO와 빠른 속도 모두 챙길 수 있었지만, 이 방식에도 한 가지 결함이 있었다. 페이지들이 빌드 시에 fetch된 데이터를 기반으로 생성되기 때문에, 빌드 이후의 변경 사항이 페이지에 반영되지 않았다. 숙소 정보를 수정해도 그 수정 사항이 다음 배포에나 반영될 수 있게 된 것이다. 물론 숙소 데이터가 자주 생성되거나 수정되지 않았기 때문에 큰 문제는 아니었지만, 변동 사항이 발생할 때마다 재배포를 하는 것은 너무 번거로운 일이었다.

ISR(Incremental-Static-Regeneration)

export async function getStaticProps({ params }) {
  const lodgment = await getLodgmentById(params.id);
  return {
    props: {
      lodgment,
    },
    // 추가된 부분
    revalidate: 3600,
  };
}

SSG 방식에서는 데이터가 생성되거나 수정될 경우 변동 사항이 적용되지 않는 문제가 발생했다. 빌드 시에 미리 정적인 HTML을 준비해두는 방식이기 때문에 다시 빌드를 하여 재배포하지 않는 이상 변동사항이 적용되지 않았다. 그렇다고 있을지 없을지 모르는 데이터 변동사항 때문에 몇 시간 단위로 재배포할 수도 없는 노릇이었다. 그래서 ISR을 통해 일정 주기로 변동사항이 반영될 수 있도록 했다. ISR 방식에서는 설정된 시간이 지난 뒤, 해당 페이지를 방문하면 다시 빌드하여 리프레시된 데이터를 가지고 해당 페이지를 재생성한다. 즉, 설정 시간 지난 후에 누군가가 페이지에 방문하면 SSR처럼 동작한 뒤, 다시 리프레시된 정적 페이지가 서빙된다.

Next js에서는 ISR을 간단히 구현할 수 있다. getStaticProps에서 return하는 객체의 revalidate 속성을 통해 시간(초 단위)을 넘겨주면 된다. revalidate로 설정된 시간이 지난 뒤 누군가가 방문하면 다시 빌드되어 해당 페이지가 갱신된다.

위와 같이 구현하면, 정적 페이지가 서빙되다가 1시간이 지난 뒤 방문한 첫 사용자는 재배포된 페이지를 보게 된다. 이때, 사용자는 SSR로 구현했을 때와 같은 느린 속도를 경험하게 된다(해당 페이지가 갱신되기 때문). 하지만 1시간에 페이지 별로 한 명씩 약간의 딜레이를 겪는 것은 큰 문제가 되지 않기 때문에 ISR을 유지하기로 결정했다.

숙소 정보와 같이 변동 사항이 자주 발생하지 않는 데이터를 다루는 페이지의 경우, ISR이 좋은 선택지가 될 수 있다.

소감

이번 계기를 통해 SSR, SSG를 구현하며 Next js의 효용을 체감할 수 있었다. 마케팅으로 투입할 비용이 충분치 않은 소규모 스타트업에서 자연 유입을 가능케하는 SEO는 매우 중요하다. Next js는 별도의 비용 없이 우리 서비스를 곳곳에 홍보할 수 있게 해주는 고마운 프레임워크이다.

기술은 비즈니스를 위해 존재한다. 그런데 어렵고 복잡한 기술이라고 반드시 비즈니스에 더 큰 도움이 되는 것이 아니며, 쉽고 간단한 기술이라고 큰 도움이 되지 않는 것도 아니다. 그런 의미에서 개발자의 역할은 해결해야 하는 문제들의 우선 순위를 잘 정렬하고 가용 리소스를 고려하여, 비용 대비 가장 큰 임팩트를 낼 수 있는 해결 방안을 고안해내는 것까지이다. SEO 구현은 어렵지 않았지만 inflow가 부족한 현 상황에서 꼭 필요한 액션이었다.

profile
개발을 위한 개발보다는 필요한 개발을!

6개의 댓글

comment-user-thumbnail
2023년 1월 6일

좋은 글 감사합니다:)

1개의 답글
comment-user-thumbnail
2023년 1월 6일

안녕하세요, 연락드리고 싶은 내용이 있는데 이메일 정보가 없어서 댓글로 남깁니다. 답글로 남겨주시면 회신드리겠습니다.

1개의 답글
comment-user-thumbnail
2023년 2월 27일

멋진 글이네요:)

1개의 답글