옴니어스 홈페이지는 2021년 3월에 리뉴얼되었습니다. 홈페이지에 필요한 컨텐츠는 컨텐츠 관리 도구(CMS: Contents Management System 로 동적으로 관리하면서도 정적페이지 생성기(Static Page Generator) 로 SEO 최적화에 성공했습니다. 이 포스팅에서는 옴니어스 홈페이지 개발 과정을 간략하게 보여드리려고 합니다.

개발 과정을 보여드리기 전에 홈페이지 개발 직후 크롬 Lighthouse를 이용한 테스트 점수를 보여드리겠습니다.

Untitled
Untitled
Untitled
Untitled

보다시피 검색엔진 최적화 부분에서 매우 높은 점수를 기록했습니다. (성능또한 현재는 많이 개선되었습니다)

서론에서 말했듯이 홈페이지에 필요한 컨텐츠를 동적으로 관리함에도 불구하고 어떻게 검색엔진 최적화 점수가 이렇게 높을수가 있을까요?

이 개발 방법을 소개하기 전에 알아야 할 몇가지 개념이 있습니다. 각 개념들을 최대한 간략하게 설명하고 넘어가겠습니다.

  • SPA (Single Page Application)

SPA란 ‘Single Page Application’ 약자이며 단일페이지로 구성된 웹 어플리케이션을 말합니다. SPA의 가장 큰 특징은 렌더링의 역할을 서버가 아닌 클라이언트(브라우저)에서 처리한다는 점입니다. 흔히 가장 많이 들어보셨을 React , Angular , Vue 를 이용하여 SPA를 개발합니다.

렌더링을 서버가 아닌 클라이언트에서 처리하는 만큼 네이티브 앱과 비슷한 수준의 사용자경험(UX)을 제공할 수 있습니다. 하지만 같은 이유로 SEO적인 측면에서는 매우 불리하게 됩니다.

SPA에서 설명했듯이 렌더링을 하는 시점에 따라 개발방식을 구분할 수 있습니다.

  • CSR (Client Side Rendering)
출처: The Benefits of Server Side Rendering Over Client Side Rendering
출처: The Benefits of Server Side Rendering Over Client Side Rendering

브라우저에서는 JS 다운로드 받을때까지 기다렸다가 앱을 실행하고 사용할 수 있게 됩니다. UX에는 유리하지만 초기 로딩이 느리고 SEO에 불리한 특징이 있습니다.

  • SSR (Server Side Rendering)
출처: The Benefits of Server Side Rendering Over Client Side Rendering
출처: The Benefits of Server Side Rendering Over Client Side Rendering

SSR의 경우에는 서버에서 렌더링 된 HTML을 다운받아 화면이 바로 보여집니다. 그 후 JS를 다운받아 앱을 실행하여 사용할 수 있게 됩니다. 초기 로딩이 빠르고 SEO에 유리하지만 UX에는 불리한 특징이 있습니다.

  • CMS (Contents Management System)

말 그대로 컨텐츠를 관리하기 위한 시스템입니다. 워드프레스 같은 예시가 있습니다. 관리자가 컨텐츠를 관리하고 프로덕트에서 필요한 시점에 이 컨텐츠를 가져와서 화면에 바인딩 하는 방식을 주로 사용합니다.

이 개념들을 기초로 몇가지 툴을 이용하여 홈페이지를 개발하였습니다.


요구사항과 개발스택

많은 미팅을 진행하면서 필요한 요구사항과 개발스택을 적절하게 정하는것이 먼저 중요합니다.

  1. 관리자가 개발자의 도움없이 컨텐츠를 수정하여 배포 할 수 있다.
  2. SEO 친화적인 웹페이지
  3. 성능이 좋은 웹페이지

이러한 요구사항이 있을때 어떠한 개발스택을 정해야 할지 고민이 되었습니다. 특히 1번은 동적인 웹페이지의 성격이지만 2번은 정적인 웹페이지의 성격입니다. 요구사항 두가지가 역설적이지만 위의 개념들과 앞으로 나올 툴을 조합하여 해결 할 수 있습니다.

정적 페이지 생성기

기본적으로 홈페이지는 프로덕트를 소개하고 고객을 모으기 위한 웹사이트입니다. 이러한 기능들을 하기 위해서 가장 중요한 부분은 SEO 라고 할 수 있습니다. 이전의 옴니어스 홈페이지는 React 로 개발되어 CSR 을 이용하고 있었기 때문에 검색엔진의 크롤러가 원활하게 크롤링 할 수 없는 구조였습니다.

당장의 미봉책으로 페이지를 단순히 hydrate(각 페이지가 build 단계에서 임시로 string화 하여 html을 만들어 냄) 하여 임시로 SEO 최적화를 진행했지만 추후에 나올 내용인 컨텐츠 관리 부분에서 문제가 있었습니다. 또한 당시에 다국어를 지원할 계획이 있었기에 더 이상 유지가 어렵다고 판단했습니다.

React 를 사용하면서 정적페이지를 생성해주는 툴은 대표적으로 Next.js, Gatsby 가 있습니다.

컨텐츠 관리

옴니어스의 홈페이지는 많은 텍스트와 이미지 그리고 영상이 있습니다. 이것들을 컨텐츠 라고 정의하고, 이 컨텐츠들을 관리자들이 개발자의 도움없이 직접 수정하고 배포할 수 있는 환경을 만드는 것이 이번 프로젝트의 가장 목표중에 하나였습니다. 이러한 목표를 달성하기 위해 CMS 라는 것을 도입하기로 결정하였고 리서치 후 Prismic을 사용하도록 결정하였습니다.

Prismic은 Custom types 이라는 시스템으로 페이지의 틀을 만들면, 이 틀을 기반으로 관리자가 텍스트, 이미지 등을 드래그앤드롭만으로 쉽게 조작할 수 있는 장점이 있습니다. 개발자는 Custom types로 만든 type의 schema 를 이용하여 저장된 컨텐츠를 쉽게 fetching 하여 사용할 수 있습니다.

Gatsby + Prismic

결론을 먼저 말하자면 옴니어스에서는 개발스택을 Gatsby + Prismic 조합으로 정했습니다.

Prismic 은 Custom types의 schema를 이용하여 GraphQL 을 사용 가능함을 확인했고,  GatsbyReactGraphQL 을 이용하여 정적페이지를 생성할 수 있기 때문에 두가지를 조합하면 동적 컨텐츠관리를 하면서 동시에 정적페이지를 생성할 수 있습니다.

옴니어스에서 고민하여 만든 파이프라인은 아래와 같습니다.

CI_CD pipeline example.jpeg

개발자의 코드 배포시에는 새로운 코드 버전이 배포될때 자동화된 배포 플로우를 구축하였습니다.

하지만 이 상태로는 이전과 다를게 없어보입니다. 우리는 동적컨텐츠를 이용하고 있고 이러한 파이프라인으로는 코드가 배포 될 때에만 컨텐츠가 최신화되기 때문입니다. 다시말해 컨텐츠 업데이트가 필요하다면 개발자가 버전만 올린 빈 커밋이 필요하다는 것입니다. 이 문제를 해결하는 키는 Prismic에 있습니다.

Untitled

Prismic에는 Webhooks 라는 기능이 있습니다. 말 그대로 특정 액션에 Webhook을 실행시켜주는 기능입니다.

Screen Shot 2022-03-22 at 12.48.24_censored.jpg

Webhooks의 좀 더 자세한 설정 화면입니다. 스크린샷 하단의 Triggers에 선택한 액션을 수행시 URLSecret 을 담은 POST 요청을 보냅니다.

예를들어 컨텐츠가 변경된 뒤 publish 하게 된다면 설정한 URLSecret 을 담아 POST 요청을 보냅니다. 이렇게 컨텐츠가 변경된것을 실시간으로 알 수 있습니다.

CI_CD pipeline example (1).jpeg

최종적으로 코드가 배포되거나 컨텐츠가 배포 될 때 두가지 상황을 모두 커버하는 파이프라인이 완성되었습니다.

컨텐츠 매니저가 컨텐츠를 업데이트했을때 설정해둔 Webhooks로 AWS Lambda에 미리 정의해 두었던 Secret 키와 함께 보냅니다. Lambda에서는 serverless function 이 실행되어 해당 Secret 이 유효한 경우 회사 내부 Jenkins 서버로 빌드 트리거를 날립니다. 빌드 트리거를 받은 Jenkins는 최신버전 컨텐츠를 가져와 정적페이지를 만들어 배포합니다.

지금까지는 어떠한 방법으로 동적컨텐츠를 정적페이지화 하였는지 파이프라인에 대해 소개해드렸습니다. 이제 조금 더 코드적인 내용의 예제를 작성해보겠습니다.


GatsbyPrismic 연동하기

Gatsby 에는 여러가지 플러그인들을 추가할 수 있는데요, 그 중에서 Prismic 에서 공식적으로 지원하는 플러그인이 있습니다.

gatsby-source-prismic 에서 자세한 정보를 확인할 수 있습니다. 핵심은 Gatsby 프로젝트에서 Prismic 플러그인을 연동하여 사용한다는 것입니다.

아래 예제는 사용중인 Gatsby 프로젝트가 있다는 가정하에 진행하겠습니다. 진행중인 프로젝트가 없다면 이 곳을 참고하여 프로젝트를 생성하시고 따라하셔도 됩니다.

먼저 진행중인 Gatsby 프로젝트에 필수 플러그인들을 설치합니다.

yarn add gatsby-source-prismic gatsby-plugin-image @prismicio/react

그리고 Prismic 서비스에 가입하고 repo를 생성합니다. 이후 프로젝트에 .env.development 파일을 생성합니다. 추후 배포시 해당 환경변수는 사용중인 빌드환경에서 빌드타임에 인젝션 해주시면 됩니다.

// .env.development

GATSBY_PRISMIC_REPO_NAME=your-repo-name
PRISMIC_ACCESS_TOKEN=your-access-token

이후 gatsby-config.js 파일을 수정합니다.

// gatsby-config.js

require('dotenv').config({
  path: `.env.${process.env.NODE_ENV}`
});

module.exports = {
  siteMetadata: {
    title: `OMNIOUS HOMEPAGE`
  },
  plugins: [
    // ...
    // ...
    {
      //  prismic 연동 플러그인
      resolve: 'gatsby-source-prismic',
      options: {
        repositoryName: process.env.GATSBY_PRISMIC_REPO_NAME,
        accessToken: process.env.PRISMIC_ACCESS_TOKEN,
        schemas: {
          home: require('./src/schemas/home.json'),
          // ...
          // ...
        },
      }
    },
    // ...
    // ...
  ]
};

위 config 파일의 schemashome 이라는 항목이 보이네요. 이 schemas 는 우리가 Prismic 에 Custom Types 로 등록한 types의 스키마입니다.

Prismic 의 Custom Types 메뉴에 들어간 뒤 home 이라는 이름으로 새로운 types를 생성해봅시다. 그리고 Build mode를 통해서 자유롭게 컨텐츠를 조절한 뒤에 JSON editor 탭으로 이동하면 그동안 정의해 둔 type의 스키마가 있습니다.

Untitled

이 스키마를 복사하고 프로젝트 ./src/schemas/{types-name}.json 형식으로 파일을 생성 한 뒤 넣어줍니다.

custom types를 생성하였다면 Prismic 의 document 메뉴로 이동하여 신규페이지를 생성하고 사용할 types를 선택합니다. 그러면 컨텐츠를 관리할 첫번째 페이지가 생성됩니다. 해당 document를 원하는 컨텐츠로 채워놓고 save - publish - publish now 합니다.

이제 로컬서버에서 확인을 해보겠습니다.

gatsby develop

Gatsby 프로젝트의 기본 실행 포트는 8000번입니다. http://localhost:8000/___graphql 플레이그라운드로 접속하여 정상적으로 연동되었는지 확인해보겠습니다.

쿼리를 이렇게 날려보겠습니다.

query MyQuery {
  prismicHome {
    dataRaw
  }
}

dataRaw는 정의된 모든 컨텐츠를 가져오는 쿼리입니다.

Untitled

쿼리의 결과가 잘 출력되나요?

이제 실제 페이지에 각 데이터를 props 로 내려주어 해당 컴포넌트에서 렌더하는 일만 남았습니다.

Gatsby 프로젝트를 사용해보신 분이라면 익숙하겠지만 src/pages 폴더 파일을 생성함으로써 빌드과정에서 해당 파일이 정적페이지로 생성됩니다. 우리 예제에서는 홈 화면을 만들고 있으므로 src/pages/index.jsx 파일을 생성하겠습니다. 그리고 src/components/Home.jsx 로 홈화면을 렌더하겠습니다.

// 1. src/components/Home.jsx

export default function Home({ data }) {
	return (
		<main>
			{ ...data }
			{ ...data }
		</main>
	)
}
// 2. src/pages/index.jsx

import React from 'react';
import Home from '@components/Home';
import { graphql } from 'gatsby';

const HomeTemplate = ({ data }) => {
  return <Home data={data} />;
};

export default HomeTemplate;

export const query = graphql`
  query {
    prismicHome {
      dataRaw
    }
  }
`;

이 두가지 컴포넌트를 간단하게 설명하자면, 1번 컴포넌트는 data props를 받아 단순히 렌더하는 컴포넌트이고 2번 컴포넌트는 prismic에 있는 데이터를 query를 이용하여 fetching해서 Home 컴포넌트에 주입해주는 컨테이너 컴포넌트입니다.

2번 컴포넌트의 query를 정의하여 export 하는것만으로 HomeTemplate의 data props로 받을수 있게됩니다. 그리고 이 data는 아까 플레이그라운드에서 확인했던 그 데이터가 됩니다. 이 데이터를 그대로 Home 컴포넌트에 주입해주고 해당 데이터를 받은 Home 컴포넌트에서는 컨텐츠를 렌더하게 됩니다.

그리고 2번 컴포넌트의 default로 export된 HomeTemplate은 Gatsby 에서 page폴더 이하 경로를 따라서 /index 페이지로 생성되게 됩니다.

Blank diagram.jpeg

지금까지의 예제를 간단하게 정리한 차트입니다.

다시한번 정리하면 query를 export 함으로써 템플릿 컴포넌트에 props로 해당 쿼리의 데이터를 주입해주고, 템플릿 컴포넌트는 다시 프레젠테이션 컴포넌트에 props로 주입해주는 모습입니다. 그리고 마지막으로 프레젠테이션 컴포넌트는 받은 props로 화면을 렌더합니다.


SEO와 메타 컴포넌트

홈페이지의 모든 페이지에는 메타정보들이 들어있습니다. 이러한 메타정보를 각 서치엔진 크롤러가 크롤링하여 보여줍니다. 따라서 모든 페이지에 필수 메타정보를 잘 정리하여 추가해주어야 합니다. 옴니어스에서는 이 메타정보를 어떻게 처리하고 있을까요?

React 기반의 프로젝트에서 가장 쉽게 메타정보를 변경할 수 있는 국룰 라이브러리가 있습니다.

react-helmet 입니다. 정말 많은 사람들이 사용하는 라이브러리인데요. 물론 저희 프로젝트에서도 적용할 수 있습니다.

yarn add react-helmet gatsby-plugin-react-helmet
// gatsby-config.js

require('dotenv').config({
  path: `.env.${process.env.NODE_ENV}`
});

module.exports = {
  siteMetadata: {
    title: `OMNIOUS HOMEPAGE`
  },
  plugins: [
    // ...
	// ...
    `gatsby-plugin-react-helmet`, // <-- React helmet 적용
    {
      //  prismic 연동 플러그인
      resolve: 'gatsby-source-prismic',
      options: {
        repositoryName: process.env.GATSBY_PRISMIC_REPO_NAME,
        accessToken: process.env.PRISMIC_ACCESS_TOKEN,
        schemas: {
          home: require('./src/schemas/home.json'),
          // ...
          // ...
        },
      }
    },
    // ...
    // ...
  ]
};

예제에서 설명했듯이 Gatsby 에서는 정말 많은 플러그인을 지원합니다. 그 중에서 react-helmet 도 예외는 아닙니다. 이제 필수 메타태그를 정의하는 컴포넌트를 만들어보겠습니다.

// src/components/Meta.jsx
import { Helmet } from 'react-helmet';

export default function Meta({ metadata }) {
	const { title, description, alternates, lang, url, image } = metadata;

	return (
    	<Helmet titleTemplate="%s">
          <html lang={`${lang.slice(0, 2)}-${lang.slice(3, lang.length).toUpperCase()}`} />
          <title>{title}</title>
          <meta name="description" content={description} />
          <link rel="image_src" href={`https://www.omnious.com${image}`} />
          <meta property="og:site_name" content="OMNIOUS" />
          <meta property="og:title" content={title} />
          <meta property="og:description" content={description} />
          <meta property="og:url" content={url.replace('_lang', lang)} />
          <meta property="og:locale" content={`${lang.slice(0, 2)}_${lang.slice(3, lang.length).toUpperCase()}`} />
          <meta property="og:type" content="website" />
          <meta property="og:image" content={`https://www.omnious.com${image}`} />
          <meta property="fb:pages" content="OMNIOUS" />
          <meta name="twitter:card" content="summary_large_image" />
          <meta name="twitter:title" content={title} />
          <meta name="twitter:description" content={description} />
          <meta name="twitter:image" content={`https://www.omnious.com${image}`} />
          <meta name="twitter:site" content="@OMNIOUS" />
          {alternates?.map(alt => {
            const altLang = `${alt.lang.slice(0, 2)}-${alt.lang.slice(3, alt.lang.length).toUpperCase()}`;
            return <link rel="alternate" href={url.replace('_lang', alt.lang)} hrefLang={altLang} key={altLang} />;
          })}
    	</Helmet>
  );
}

이런식으로 필수 메타정보를 렌더하는 컴포넌트를 생성하였습니다.

// src/components/Home.jsx
import Meta from '@components/Meta';

export default function Home({ data }) {
	return (
		<>
			<main>
				{ ...data }
				{ ...data }
			</main>
			<Meta metadata={ ... } />
		</>
	)
}

그리고 위와같이 필요한 컴포넌트에서 메타정보를 같이 렌더해주면 간단하게 SEO 설정을 할 수 있게됩니다.


마치며

지금까지 옴니어스의 홈페이지 개발과정을 간략하게 보여드리고 예제까지 작성해보았습니다.

위 내용들은 정말 핵심적인 내용들만 간략하게 적은 내용들이고, 사실은 컨텐츠 작성중 프리뷰 기능이나 다국어 등 다른 여러가지 기능들도 추가로 개발되어 있습니다. 또한 예제는 Javascript로 작성했지만 실제 개발 코드는 Typescript로 작성되어있습니다.

결과물이 궁금하시다면 옴니어스 홈페이지 에 오셔서 구경해보시면 좋을 것 같습니다.