안녕하세요! WEB 김채현입니다.
티켓 사이트에서 공연 페이지를 공유할 때는 공연의 포스터가 공유가 되는 것이 일반적인데요,
비트는 공연 페이지를 공유해도 BEAT 이미지가 보여지고 있습니다.
이유는 바로 제가 index.html을 아래와 같이 작성했기 때문입니다.
<meta property="og:image" content="https://www.beatlive.kr/og_img.png" />
React는 SPA로 CSR방식으로 렌더링을 하기 때문에 하나의 index.html을 두고, 자바스크립트를 활용하여 데이터만 변경됩니다.
공통적으로 적용되는 index.html에 OG 이미지를 "og_img.png"로 지정해주었기 때문에
카카오톡에서 페이지를 공유할 때 모든 페이지에서 항상 동일한 BEAT 이미지나 제목이 표시되는 문제가 발생하고 있었습니다.
그렇다면 어떻게 동적으로 각 공연에 맞는 OG를 설정할 수 있을까요?
해결 방법은 바로 react-helmet-async와 react-snap입니다.
🪖react-helmet-async
react-helmet은 동적으로 SEO에 필요한 메타태그들을 쉽게 변경할 수 있게 도와주는 라이브러리이고,
react-helmet-async는 페이지 렌더링 시점에 동적으로 HTML의 <head> 영역을 업데이트할 수 있도록 도와주는 라이브러리로 동적 meta 태그를 설정할 수 있습니다.
두 라이브러리를 비교해보자면...
react-helmet-async는 react-helmet를 기반으로 만들어진 라이브러리로 react-helmet의 주요 기능을 모두 포함하면서 동시에 비동기 방식으로 작동합니다.
또한, 기존의 react-helmet은 thread-safe()하지 않은 react-side-effect에 의존했는데,react-helmet-async는 react-helmet과 다르게 react-side-effect에 의존하지 않고 안전한 비동기 처리를 지원합니다.서버에서 비동기식 작업을 수행하는 경우 요청별로 데이터를 캡슐화하는 것을 react-helmet-async에서 지원한다는 의미입니다.
react-helmet는 마지막 업데이트가 현재 기준으로 4년 전인 반면, react-helmet-async는 3달 전이었습니다.
즉, react-helmet-async 라이브러리를 통해 안전하게 각 공연 페이지에 맞는 OG 태그를 설정할 수 있습니다.
react-helmet-async 라이브러리를 사용하면 react-helmet과는 다르게
data-rh="true" 라는 속성이 자동으로 생깁니다.
react-helmet-async가 관리하는 DOM 노드를 식별하는 데 사용되는데, 이렇게 식별된 노드들은
react-helmet-async에 의해 관리되고, 중복되지 않도록 자동으로 업데이트됩니다.
1️⃣라이브러리를 설치한 후, App을 HelmetProvider로 감싸주었습니다.
HTML <head> 요소에 대한 제어를 모든 하위 컴포넌트에 적용하기 위해 최상단에 작성했습니다.
2️⃣Helmet으로 동적으로 meta 태그를 설정해주었습니다.
공연 상세 정보 페이지인 Gig.tsx에 아래처럼 Helmet을 작성했습니다.
혹시 공통 컴포넌트로 빼고 싶다면?
props로 넘기는 형태로 만들면 됩니다!
📸react-snap
동적으로 메타 태그를 설정해주었지만 끝나지 않았습니다.
페이지별로 html 파일이 생성되는 것이 아니기 때문에 브라우저의 크롤러는 index.html만 인식하기 때문에
url을 공유하였을 때 동적으로 변한 meta 태그가 보이지 않는 문제가 발생합니다.
이 문제를 react-snap을 사용하면 크롤러가 페이지를 제대로 인식할 수 있어 해결할 수 있습니다.
react-snap은 빌드 시점에 정적 HTML 파일을 생성하여 초기 렌더링을 위한 메타 데이터를 포함합니다.
즉, 페이지별로 index.html 파일이 생성됩니다.
또한 react-snap은 pre-rendering을 지원합니다.
pre-rendering?
SPA은 빠르게 빌드할 수 있는 장점이 있지만, 로드되어 보여줄 준비가 되지 않는 단점이 있습니다.
사전 렌더링은 웹 크롤러가 볼 수 있도록 페이지의 모든 요소를 사전로드하는 프로세스를 의미합니다.
prerender 서비스는 페이지 요청을 낚아채어 사용자가 크롤러인지 여부를 확인하여 크롤러인 경우 캐시 된 버전의 페이지를 전달하여 줍니다.
즉, 크롤러를 위한 최적화 작업을 하는 것이 prerender의 핵심이라고 할 수 있습니다.
그러나 주의할 점이 있는데요,
react-snap을 이용한 pre-rendering은 단순히 빌드 시점에 렌더링된 화면을 마치 스크린샷을 찍듯이 크롤링한 것 뿐이기 때문에 어떠한 동작도 하지 않습니다.
1️⃣라이브러리를 설치한 후, 빌드 후에 react-snap 을 실행하기 위해서 package.json에 postbuild를 작성합니다.
React를 사용하여 프로젝트를 한다면, react-snap과 react-helmet-async를 통해 SEO에 불리하다는 단점을 어느 정도는 극복할 수 있을 것이다.
2️⃣자식요소가 있으면 hydrateRoot를 사용합니다.
⚠️gig에 해당하는 index.html이 생기지 않는다...!
이렇게 설정을 해주고 yarn build를 하면 원래 gig에 해당하는 index가 추가로 생겨야 하는데
index.html이 하나밖에 생기지 않았습니다.
package.json에 reactSnap 옵션을 주기도 해봤고,
react-snap이 vite랑 잘 안맞나..? 라는 생각이 들어서 vite.config.ts에 build를 추가하기도 해봤습니다.
왜 react-snap에서는 되지 않았을까?
react-snap은 react-scripts를 사용하는 CRA 프로젝트에서 작동하도록 만들어진 도구이고,
BEAT에서 사용하는 vite는 ESmodules 기반이라 react-snap과는 다르게 동작한다고 합니다!
react-snap은 postbuild 단계에서 dist/ 폴더를 스캔해서 정적 HTML을 생성하는 방식이고,
vite는 rollup을 기반으로 번들링하기 때문에 dist/ 폴더 구조가 CRA와 다르게 생성되기 때문에
react-snap이 빌드된 파일을 정상적으로 탐색하지 못하고 index.html 하나만을 생성하게 되었던 것....
🤥puppeteer와 prerender
puppeteer는 구글에서 만든 노드 라이브러리로 Headless Chrome(백그라운드에서 작동하는 브라우저) 또는 Chrominum을 제어할 수 있습니다. 따라서 실제로 브라우저 환경에서 페이지를 로드하고 정적 HTML을 생성할 수 있습니다.
puppeteer를 사용하면 화면을 스크린샷하거나 PDF를 생성할 수 있고, SPA를 크롤링하여 사전에 렌더링할 수 있습니다.
1️⃣ yarn add --dev puppeteer @prerenderer/renderer-puppeteer @prerenderer/rollup-plugin
2️⃣ vite.config.ts에 prerender를 작성합니다.
rendererOptions?
maxConcurrentRoutes는 한 번에 몇 개의 경로를 동시에 렌더링할지를 설정하는 옵션입니다.
기본값은 0(제한없음)이지만 react-helmet에서 충돌이 발생할 수 있어
하나의 경로를 완전히 렌더링한 후에 다음 경로로 넘어가는 것이 안전할 수 있습니다.
시간이 좀 더 걸리더라도 하나씩 확실하게 렌더링하도록 설정해두었습니다.
또한, renderAfterTime은 각 페이지가 렌더링을 시작한 후 기다리는 시간입니다.
너무 낮게 설정하거나 설정하지 않을 경우, 페이지가 완전히 로드되기 전에 렌더링이 완료될 수 있습니다.
서버와 통신이 완료되기 전에 렌더링이 진행되지 않도록 적절히 설정해야 합니다.
이렇게 코드를 작성하고 yarn build를 하면 아래처럼 gig/53에 index.html이 제대로 생성됩니다.
그런데 문제는 gig에 id를 주어야 사전 렌더링이 되는데, id를 1~max로 반복문을 돌리는 것은 비효율적이라고 생각했습니다.
공연이 삭제되기도 해서 필요없이 사전 렌더링을 할 수도 있고, 가장 큰 문제는 공연이 많아지면 max를 넘어갈 수도 있기 때문입니다.
인터파크 티켓에서도 예매가 종료된 공연 페이지는 meta 태그가 적용이 되지 않는 것을 확인할 수 있었습니다.
그렇다면 예매가 가능한 공연의 performanceId를 미리 가져와서 그것만 사전 렌더링을 해주면 되지 않을까요??
3️⃣fetchPerformanceIds 함수 작성
이때 url은 .env의 VITE_API_BASE_URL을 넘겨준 것입니다.
vite.config.ts에서 .env를 바로 접근할 수 없기 때문에
loadEnv를 사용해서 url을 넘겨주었습니다.
최종 vite.config.ts 파일은 아래처럼 작성했습니다.
정리
react-helmet-async와 puppeteer을 사용하면 동적으로 meta 태그를 설정할 수 있습니다.
1️⃣ HelmetProvider로 App을 감싸고
2️⃣ Helmet으로 동적으로 meta 태그를 설정하고
3️⃣ vite.config.ts에서 prerender를 알맞게 작성합니다.
아래처럼 meta 태그가 잘 적용됩니다!!
사실 이 코드를 vercel로 배포할 때 많은 에러가 발생했는데...
다음 글에서 이어서 설명하겠습니다.
to be continued...
WEB_김채현
공연 등록하기를 맡은 FE 개발자 김채현입니다.
'WEB' 카테고리의 다른 글
구글 애널리틱스(GA) 도입해서 사용자의 유입을 알아보자! (0) | 2024.07.23 |
---|---|
프론트엔드에서 production 서버와 development 서버 분리하기! (0) | 2024.07.23 |
캐러셀... 너 뭔데 나를 이렇게 힘들게 해... (0) | 2024.07.19 |
상태 관리 라이브러리 (Jotai vs Zustand) (0) | 2024.07.19 |
못말리는 Input 달래주기 2편 (이모지 입력) (1) | 2024.07.19 |