-
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 데이터가 포함된 다이나믹 라우팅 페이지 작업 순서
- _document.tsx 에서 중복을 유발하는 대표 title, description 지우기
- next/head 를 사용하기 위한 재사용 컴포넌트 만들기
- 데이터 삽입 및 테스트하기
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
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
의도대로 동작하고 있습니다!
'React.js & Next.js' 카테고리의 다른 글
냅다 시작하는 리액트 쿼리 (예제편) (0) 2022.12.27 예제로 배우는 react context (0) 2022.12.18 Next.js - next/image blur 사용하기 (0) 2022.10.19 Next.js - middleware 사용하기 (로그인 연동하기) (0) 2022.09.25 Next.js - middleware 사용하기 (기본편) (0) 2022.09.25