react-notion-x image

8/23/2022

작성자 : 홍원배

react-notion-x 이미지가 로드 되지 않는 문제

 
react-notion-x는 Notion API를 통해 가져온 데이터로 화면을 렌더링 해주는 라이브러리이다.
 
// api.js import { NotionAPI } from 'notion-client' const notion = new NotionAPI() const recordMap = await notion.getPage('067dd719a912471ea9a3ac10710e7fdf') // app.jsx import * as React from 'react' import { NotionRenderer } from 'react-notion-x' export default ({ recordMap }) => ( <NotionRenderer recordMap={recordMap} fullPage={true} darkMode={false} /> )
react-notion-x의 기본 template
위와 같이 Notion 비공식 API를 통해 받아온 데이터, recordMap은 NotionRenderer 컴포넌트에 props로 전달되어 화면을 렌더링 해준다
 
 
하지만, 렌더링이 되어야할 포스트 상세 내용의 이미지가 간헐적으로 그려지지 않는 문제가 발생했다.
(현재 Notion 블로그는 SSG 렌더링 방식을 사용해 vecel 서버에서 pre-rendering을 통해 html을 구성해 클라이언트로 보내주고 있다)
 
이미지가 로딩 되어지지 않는다
이미지가 로딩 되어지지 않는다
‘간헐적’이라고 표현한 것은 배포시에는 정상 작동했는데 나중에 다시 확인해보니 이미지가 로드 되지 않았기 때문이다.
 
 

왜 그럴까?

처음에는 단순히 CORS로 인해 접근제한이 막혔다고 생각했지만 아니라고 판단했다. CORS가 문제 였다면 image뿐만아니라 code나 text 같은 다른 요소도 모두 렌더링 되지 않았을 것이기 때문이다.
 
이미지 에러를 확인하는 중에 눈길이 끄는 것이 있다. 바로 요청이 만료 되었다는 것이다.
내 소유의 노션페이지인데 .. ?
내 소유의 노션페이지인데 .. ?
 
Request has expired 메시지에 이어 X-Amz-Expires라는 헤더는 Amazon S3를 사용하는 노션 서버에 접근할 수 있는 시간제한이다. 86400초였으면 24시간, 즉 하루동안 접근이 가능했었다는 것을 알 수 있다.
 
오류보고는 간헐적으로 동작했던 이유에 대해서 합리적인 추측을 하게 해준다. 요청이 만료되었기 때문에 처음에 동작시에는 정상 작동했으나 추후에 에러가 발생한 것이다. 그렇다면, 노션 API 서버는 이미지의 접근권한에 시간제한을 걸어두었단 말일까? 공개된 페이지로 요청했을텐데.. 내가 놓치고 있는 부분이 있나 다시 한번 관련문서를 꼼꼼히 읽어보자
 
Another major factor for perf comes from images hosted by Notion. They're generally unoptimized, improperly sized, and not cacheable because Notion has to deal with fine-grained access control that users can change at any time. You can override the default mapImageUrl function on NotionRenderer to add caching via a CDN like Cloudflare Workers, which is what Notion X does for optimal page load speeds.
… 노션은 유저가 언제든지 변경할 수 있는 세분화된 접근 제한을 가지고 있기 때문에 노션 이미지는 최적화 되지 않고 캐시도 되지 않습니다. mapImageUrl 함수를 NotionRenderer에다가 재정의함으로서 CDN을 통해 캐시를 적용할 수 있습니다…
 
 
아하! 노션은 자주 변경될 수 있는 접근제한 (노션 페이지별로 share 토글을 통해 공유했다가 해제하는 기능)으로 인해 cache 기능을 지원하지 않는다는 것을 알게 되었다. 그렇다면 S3의 SignedURL이 만료되더라도 CDN을 통해 fresh한 상태의 이미지를 유지하면 되지 않을까? 가정이 맞다면, 어떤 CDN을 이용해야할까?
 
 
CDN을 사용하기 앞서 단계적으로 고민해보자. 먼저, 여기서 말하는 mapImageUrl은 무엇을 하는 함수일까? react-notion-x 공식문서에서는 이것에 대해서 정확한 설명을 해주지 않고 있다. 코드를 한번 직접 보자.
 
코드를 보자
코드를 보자
 
import { Block } from 'notion-types' export const defaultMapImageUrl = ( url: string, block: Block ): string | null => { if (!url) { return null } if (url.startsWith('data:')) { return url } // more recent versions of notion don't proxy unsplash images if (url.startsWith('https://images.unsplash.com')) { return url } try { const u = new URL(url) if ( u.pathname.startsWith('/secure.notion-static.com') && u.hostname.endsWith('.amazonaws.com') ) { if ( u.searchParams.has('X-Amz-Credential') && u.searchParams.has('X-Amz-Signature') && u.searchParams.has('X-Amz-Algorithm') ) { // if the URL is already signed, then use it as-is return url } } } catch { // ignore invalid urls } if (url.startsWith('/images')) { url = `https://www.notion.so${url}` } url = `https://www.notion.so${ url.startsWith('/image') ? url : `/image/${encodeURIComponent(url)}` }` const notionImageUrlV2 = new URL(url) let table = block.parent_table === 'space' ? 'block' : block.parent_table if (table === 'collection') { table = 'block' } notionImageUrlV2.searchParams.set('table', table) notionImageUrlV2.searchParams.set('id', block.id) notionImageUrlV2.searchParams.set('cache', 'v2') url = notionImageUrlV2.toString() return url }
https://github.com/NotionX/react-notion-x/blob/master/packages/react-notion-x/src/map-image-url.ts
 
위 코드는 <NotionRenderer />의 mapImageUrl 함수내에서 동작하는 함수인defaultMapImageUrl이다. 이미지 url들과 block(노션의 컨텐츠 하나하나는 블록, 여기선 이미지 블록들)을 매개변수로 입력받아 CDN이 연결된 url으로 mapping하는 기능을 하는 것 같다.
 
코드를 찬찬히 보니 어떤식으로 override해야할지 힌트가 보이는것 같다.
if (url.startsWith('/images')) { url = `https://www.notion.so${url}` } url = `https://www.notion.so${ url.startsWith('/image') ? url : `/image/${encodeURIComponent(url)}` }`
 
notion 홈페이지를 CDN으로 이용하여 Signed URL을 매핑하고 있다. 이 방식을 이용해보면 어떨까?
 
응용해서 적용을 해보자
 
// [id].jsx - 포스트 컴포넌트 import { defaultMapImageUrl, MapImageUrlFn, NotionRenderer, } from "react-notion-x"; export default function Post({ recordMap, post }: Ipost) { const mapImageUrl: MapImageUrlFn = (url, block) => { const u = new URL(url); if ( u.pathname.startsWith("/secure.notion-static.com") && u.hostname.endsWith(".amazonaws.com") ) { if ( u.searchParams.has("X-Amz-Credential") && u.searchParams.has("X-Amz-Signature") && u.searchParams.has("X-Amz-Algorithm") ) { url = "/image/" + encodeURIComponent(url); } } return defaultMapImageUrl(url, block)!; }; return { <NotionRenderer recordMap={recordMap} fullPage={false} darkMode={true} components={{ Code, nextImage: Image, nextLink: Link, }} mapImageUrl={mapImageUrl} /> .. }
 
<NotionRenderer />에 커스텀한 mapImageUrl 함수를 Override하였다. mapIamgeUrl 함수는 새로운 url을 매핑하는 기능을 하는데, amazonaws 서버를 통한 Signed URL일 경우 기존의 주소를 ‘/image/’가 붙은 형태로 바꿔주고, 이것이 defaultMapImageUrl 함수의 인자로 전해져서 ‘https://www.notion.so’을 CDN으로 이용하는 형태로 바꿔주게 된다.
 
이미지가 notion.so를 통해 정상 출력된다
이미지가 notion.so를 통해 정상 출력된다
SRC의 경로가 https://www.notion.so/image/https$3a~~ (뒷부분은 Amazon S3 Singed URL)로 바뀌고 이미지를 정상적으로 받아올 수 있다.
 

Vercel이 이미지를 갱신해 줄 수는 없을까?

정리해보자.
문제의 원인은 노션에서는 빈번한 접근제한 변환 등의 문제로 인해 이미지를 캐시 해두지 않는다는 것이다. 그렇기에 일정한 주기를 가지고 새로운 요청을 통해서 신선한 이미지를 제공 받을 수 있게 해주는 CDN과 같은 서버가 필요했고, www.notion.so의 서버를 우회해서 공개 서비스중인 이미지를 받아오게 만들었다.
 
그렇다면, 다른 방법으로 vercel을 이용해서 이미지를 갱신할 수는 없을까?
 
위와 같이 설정한 이유는 결국 블로그는 정적 사이트이기 때문이다. 캐시되지 않는 최신의 이미지를 받아올 수 없었기 때문인데, ISR의 revalidate 기능을 통해 주기적으로 재빌드 해준다면, 이미지는 정상 출력 된다.
 
    태그 :