ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Next.js - 다양한 SNS 플랫폼에 대응할 수 있는 SEO 도입하기
    React.js & Next.js 2022. 10. 21. 21:12

    현재 상황

    제일 상위 (public) 디렉토리 Head 에 걸어놓은 대표 meta 태그로 인해, 특정 SNS에서 설정한 description이 노출되지 않은 상황입니다.

    디스코드 / 슬랙

     

    디스코드와 슬랙 모두 트위터 태그의 영향을 받습니다. 하지만 디스코드의 경우에는 하위 페이지에 대한 메터태그가 노출되지 않는 상황이고, 슬랙은 노출되는 상황입니다. 

     

    카카오톡, 인스타그램, 페이스 북의 경우에는 모두 의도대로 노출되고 있으나 한국 외에 사용하고 있는 SNS에 대한 SEO도 신경 써야 하기 때문에 Next.js 프레임워크 도입부터는 이전과 다른 방향으로 적용할 예정입니다.

     

    플랫폼마다 적용되어 미리보기로 보이는 메터 태그의 정보가 다양하기 때문에 모든 플랫폼에서 원하는 미리보기를 만들 수는 없습니다. 하지만, 범용적으로 다양한 플랫폼에 적용될 수 있도록 만든다면 우리가 만든 콘텐츠들이 더욱 보기 쉽고 한눈에 정보를 받아들일 수 있을 것입니다.

    문제 해결하기

    문제가 확인되었기 때문에 Next.js 에서도 최상위 파일에 meta 태그를 설정할 경우 디스코드에서 덮어 씌워지는지 먼저 확인이 필요했습니다. 만약, Next.js 에서도 동일한 문제가 발견된다면 최상위 파일에는 favicon 등의 데이터만 넣어주고 pages 경로에 개별 meta 태그를 넣어줄 생각입니다.

     

    테스트에 사용된 레포지토리 구조는 같습니다.

     

    실제 프로덕션에 사용되는 구조가 아니며, 단순 vercel을 통한 빠른 배포로 SEO를 확인하기 위함입니다.

    📦
    ├─ .eslintrc.json
    ├─ .gitignore
    ├─ .prettierrc
    ├─ README.md
    ├─ next.config.js
    ├─ package.json
    ├─ public
    │  ├─ favicon.ico
    │  └─ vercel.svg
    ├─ src
    │  ├─ api
    │  │  └─ hello.ts
    │  ├─ components
    │  │  ├─ HeadMeta
    │  │  │  └─ index.tsx
    │  │  ├─ Header
    │  │  │  ├─ index.styles.tsx
    │  │  │  └─ index.tsx
    │  │  └─ Movie
    │  │     ├─ index.styles.tsx
    │  │     └─ index.tsx
    │  ├─ config
    │  │  └─ index.tsx
    │  ├─ hooks
    │  │  ├─ index.tsx
    │  │  └─ movie
    │  │     └─ index.tsx
    │  ├─ pages
    │  │  ├─ _app.tsx
    │  │  ├─ _document.tsx
    │  │  ├─ _error.tsx
    │  │  ├─ image-test
    │  │  │  └─ index.tsx
    │  │  ├─ index.tsx
    │  │  ├─ movie
    │  │  │  ├─ [name].tsx
    │  │  │  ├─ avengers
    │  │  │  │  └─ index.tsx
    │  │  │  ├─ index.tsx
    │  │  │  └─ spiderman
    │  │  │     └─ index.tsx
    │  │  └─ react-query
    │  │     └─ index.tsx
    │  ├─ styles
    │  │  └─ GlobalStyle.tsx
    │  └─ types
    │     ├─ index.ts
    │     └─ movie
    │        └─ index.ts
    ├─ tsconfig.json
    └─ yarn.lock
    

    ©generated by Project Tree Generator

     

    우리는 기본적으로 pages/_document.tsx 파일에 next/head 를 사용하여 meta 태그를 설정할 수 있습니다. 스타일드 컴포넌트를 SSR 을 위해 사용하였고, 이 부분은 현재 상황에 필요한 로직이 아니기 때문에 … 표시로 대체하였습니다.

    import Document, { Html, Head, Main, NextScript, DocumentContext } from 'next/document';
    
    class MyDocument extends Document {
      ...
        // styled-components 코드
    
      render() {
        return (
          <Html lang="ko">
            <Head>
              <meta name="title" content="next" />
              <link rel="icon" href="/favicon.ico" />
                        <meta name="description" content="next js playgroud" />
              <meta
                property="og:image"
                content="https://d28btnt2z9x7nc.cloudfront.net/static/logo/logo_2.png"
              />
            </Head>
            <body>
              <Main />
              <NextScript />
            </body>
          </Html>
        );
      }
    }
    
    export default MyDocument; 

    확인해 본 결과 기존 리액트(CSR)의 public 환경에서 주었던 것처럼 슬랙, 카카오톡, 페이스 북 등은 문제가 없었지만, 디스코드에서 최상위 메터태그의 title과 description이 나오는 것을 발견했습니다. 따라서 각 페이지 별로 meta 태그를 설정해주기로 결정했습니다.

     

    현재 구조에서의 view를 담당하고 있는 pages 폴더의 하위 페이지들을 보겠습니다.

    ├─ pages
    │  ├─ _app.tsx
    │  ├─ _document.tsx
    │  ├─ _error.tsx
    │  ├─ image-test
    │  │  └─ index.tsx
    │  ├─ index.tsx
    │  ├─ movie
    │  │  ├─ [name].tsx
    │  │  ├─ avengers
    │  │  │  └─ index.tsx
    │  │  ├─ index.tsx
    │  │  └─ spiderman
    │  │     └─ index.tsx
    │  └─ react-query
    │     └─ index.tsx

    구조적 설계에 의해 필요한 파일을 제외하고 테스트를 위해 사용한 페이지는 다음과 같습니다

    디렉토리/ 파일명 설명
    index.tsx 기본 메인 페이지
    movie/index.tsx 영화 검색을 위한 메인 페이지
    movie/spiderman.tsx getStaticProps로 만들어 놓은 SSG 데이터가 포함된 페이지
    movie/[name].tsx getServerSideProps로 만들어 놓은 SSR 데이터가 포함된 다이나믹 라우팅 페이지

    작업 순서

    1. _document.tsx 에서 중복을 유발하는 대표 title, description 지우기
    2. next/head 를 사용하기 위한 재사용 컴포넌트 만들기
    3. 데이터 삽입 및 테스트하기

    1. _document.tsx 에서 중복을 유발하는 대표 title, description 지우기

    favicon을 제외한 이전 title, description, og:image 등을 제거하였습니다.

    import Document, { Html, Head, Main, NextScript, DocumentContext } from 'next/document';
    
    class MyDocument extends Document {
        ...
    
      render() {
        return (
          <Html lang="ko">
            <Head>
              <link rel="icon" href="/favicon.ico" />
            </Head>
            <body>
              <Main />
              <NextScript />
            </body>
          </Html>
        );
      }
    }
    
    export default MyDocument;

    2. next/head 를 사용하기 위한 재사용 컴포넌트 만들기

    페이지가 많지 않을 경우 각 페이지마다 next/head 를 사용하여 직접 정의하는 것도 나쁘지 않지만, 수많은 twitter 태그, og 태그, 기본 메터 태그를 관리하기 위해서는 재사용 가능한 컴포넌트를 만드는 것이 효율적입니다.

    <Head>
      <title>Movie | Spiderman</title>
      <meta name="description" content="Movie | Spiderman"></meta>
    </Head>

    필요한 속성들을 props로 넣을 수 있도록 조건부 렌더링을 넣은 뒤 없을 경우에는 default 로 설정을 해두어 빈 공백으로 나오지 않도록 적용하였습니다.

    import Head from 'next/head';
    
    interface HeadMeta {
      title: string;
      description: string;
      url: string;
      image: string;
    }
    
    const HeadMeta = ({ title, description, url, image }: HeadMeta) => {
      return (
        <Head>
          <title>{title || 'next'}</title>
          <meta name="description" content={description || 'next wave'} />
          <meta name="viewport" content="initial-scale=1.0, width=device-width" />
    
          <meta property="og:title" content={title || 'next'} />
          <meta property="og:type" content="website" />
          <meta
            property="og:url"
            content={url || 'https://next-playground-kappa.vercel.app'}
          />
          <meta
            property="og:image"
            content={image || 'https://d28btnt2z9x7nc.cloudfront.net/static/logo/logo_2.png'}
          />
          <meta property="og:article:author" content="next" />
    
          <meta name="twitter:card" content="summary_large_image" />
          <meta name="twitter:title" content="original" data-rh="true" />
          <meta name="twitter:description" content={description || 'next wave'} />
          <meta
            name="twitter:site"
            content="@https://next-playground-kappa.vercel.app next"
          />
          <meta
            name="twitter:image"
            content={image || 'https://d28btnt2z9x7nc.cloudfront.net/static/logo/logo_2.png'}
          />
        </Head>
      );
    };
    
    export default HeadMeta; 

    각 메터태그에 대한 정보는 이전 게시물을 확인해주세요!

     

    트래픽을 쓸어 담는 검색엔진 최적화 - chapter 3, 테크니컬 SEO

     

    트래픽을 쓸어 담는 검색엔진 최적화 - chapter 3, 테크니컬 SEO

    본문은 서적 '트래픽을 쓸어 담는 검색 엔진 최적화' 를 읽고 남기는 요약본입니다. 더 자세한 정보를 얻기 원하신다면 해당 서적을 구매하시는 것을 추천드립니다. 이전 글 보고 오기 트래픽을

    junheedot.tistory.com

    3. 데이터 삽입 및 테스트하기

    pages/index.tsx

    메인 인덱스 페이지에는 HeadMeta 컴포넌트의 기본 설정이 잘 동작하는지 확인하기 위해 props를 다 비워놓은 상태로 전달하였습니다.

    const Home: NextPage = () => {
      return (
        <>
          <HeadMeta title="" description="" image="" url="" />
    
          <div>
            <h1>Hello Home</h1>
          </div>
        </>
      );
    };
    
    export default Home;

    pages/movies/index.tsx

    movies 페이지에는 기본적인 설정을 넣어주었습니다.

    const Movie = () => {
      ...
      return (
        <>
          <HeadMeta
            title="movie search"
            description="you guys can search movie"
            image=""
            url={`https://next-playground-kappa.vercel.app/movie`}
          />
    
          <div>
            <h1>Hello Movie</h1>
            <form onSubmit={onSubmit}>
              <input type="search" value={input} onChange={(e) => setInput(e.target.value)} />
    
              <input type="submit" value="search" />
            </form>
          </div>
        </>
      );
    };
    
    export default Movie;

    pages/movies/spiderman/index.tsx

    SSG로 생성된 페이지에 대해서는 전달받은 첫 번째 결과물의 이미지를 넣어주었습니다.

    interface IMoviePosts {
      errorCode: number | boolean;
      data: ImovieData;
      blurData: blurResults[];
    }
    
    const MoviePosts = ({ errorCode, data, blurData }: IMoviePosts) => {
      ...
      return (
        <>
          <HeadMeta
            title="Movie | Spiderman"
            description="The results are all about the spiderman"
            url={`https://next-playground-kappa.vercel.app/spiderman`}
            image={`https://image.tmdb.org/t/p/w500${blurData[0].backdrop_path}`}
          />
    
          <MoviePost blurData={blurData} />
        </>
      );
    };
    
    export default MoviePosts;
    
    export async function getStaticProps() {
        ...
      return { props: { errorCode, data, blurData: posterWithBlurURL } };
    }

    pages/movies/[name].tsx

    다이나믹 라우팅 페이지에는 context로 전달받은 context.params.name 을 통해 title과 description, url 의 이름을 동적으로 생성할 수 있도록 적용하였습니다.

    interface IMoviePosts {
      errorCode: number | boolean;
      blurData: blurResults[];
      params: ParsedUrlQuery | undefined;
    }
    
    const MoviePosts = ({ errorCode, blurData, params }: IMoviePosts) => {
      ...
    
      return (
        <>
          <HeadMeta
            title={`Movie | ${params?.name}`}
            description={`${params?.name}에 대한 검색 결과입니다.`}
            url={`https://next-playground-kappa.vercel.app/movie/${params?.name}`}
            image={`https://image.tmdb.org/t/p/w500${blurData[0].backdrop_path}`}
          />
    
          <h1>{params?.name}</h1>
    
          <MoviePost blurData={blurData} />
        </>
      );
    };
    
    export default MoviePosts;
    
    export async function getServerSideProps(context: GetServerSidePropsContext) {
      ...
      return {
        props: {
          errorCode,
          blurData: posterWithBlurURL,
          params,
        },
      };
    }

    결과 보기 (디스코드/ 슬랙)

    pages/index.tsx

    pages/movies/index.tsx

    pages/movies/spiderman/index.tsx

    pages/movies/[name].tsx

    의도대로 동작하고 있습니다!

    댓글

Designed by Tistory.