새 블로그 개발일지 - 5: react-query

react-query 를 이용하여 포스트 페이지를 구현해보자

포스트 API 서버코드 작성하기

// pages/api/post.ts import { Client } from '@notionhq/client'; import { NotionAPI } from 'notion-client'; import type { IAPIError, Response } from '../../interfaces'; import type { NextApiRequest, NextApiResponse } from 'next'; import type { PageObjectResponse } from '@notionhq/client/build/src/api-endpoints'; import type { ExtendedRecordMap } from 'notion-types'; export default function handler( req: NextApiRequest, res: NextApiResponse< | Response<{ notionPage: ExtendedRecordMap; post: PageObjectResponse; }> | IAPIError > ) { // ... // ... const page_id = req.query.id as string; const notion = new Client({ auth }); (async () => { try { if (req.method === 'GET') { const response = (await notion.pages.retrieve({ page_id })) as PageObjectResponse; const notionPage = await notionRenderClient.getPage(page_id); const data = { notionPage, post: response }; res.status(200).json({ data, status: 'ok', error: null }); } else { res.status(405).json({}); return; } } catch (e) { const error = e as any; const { status, body } = error; res.status(status).json(JSON.parse(body)); } })(); }
이 전에 사용했던 posts.ts 파일과 유사하지만 여기서는 req 의 아이디를 추출하여 노션페이지를 각각 추출한다.
여기서 우리가 사용할 라이브러리가 있다.
 

react-notion-x

노션에 작성한 컨텐츠를 직접 렌더하지 않고 react-notion-x 라는 라이브러리를 이용하여 렌더할것이다.
yarn add react-notion-x
 
위 예시에서 아래와 같은 코드가 있다.
const notionPage = await notionRenderClient.getPage(page_id);
이 코드를 이용하여 해당 페이지를 렌더링 할 내용을 가져온다.
 
const response = (await notion.pages.retrieve({ page_id })) as PageObjectResponse;
그리고 이 부분에서 페이지의 상세 정보를 가져온다. 이 정보는 자세히보기 페이지 커버이미지 타이틀을 표시하기 위함이다. 사실 react-notion-x 에서 fullPage 라는 프로퍼티로 모든걸 해결 할 수 있긴 하지만, 내 블로그 테마와 살짝 어긋나는 부분이 있기 때문에 포스트의 제목이나 커버이미지는 따로 가져 올 계획이다.
 

클라이언트 코드 작성하기

// core/apis/posts.ts import { PageObjectResponse } from '@notionhq/client/build/src/api-endpoints'; import { request } from './'; import type { ExtendedRecordMap } from 'notion-types'; export const postApis = { // ... // ... getPost: (id: string) => request<{ notionPage: ExtendedRecordMap; post: PageObjectResponse; }>({ url: '/post', method: 'GET', params: { id } }) };
서버 코드쪽에 post.ts 로 포스트를 가져오는 api도 만들었으니 클라이언트 쪽에도 추가해준다. 이렇게 getPost 함수를 추가하였다.
 
// core/queries/posts.ts import { useQuery, UseQueryOptions } from 'react-query'; import { postApis } from '../apis/posts'; import type { PageObjectResponse } from '@notionhq/client/build/src/api-endpoints'; import type { Response } from '../../interfaces'; import type { ExtendedRecordMap } from 'notion-types'; export const postsQueryKey = { posts: () => ['posts'] as const, post: (id: string) => ['post', id] as const }; // ... // ... export const useGetPost = ( id: string, options?: UseQueryOptions< Response<{ notionPage: ExtendedRecordMap; post: PageObjectResponse; }> > ) => { return useQuery( postsQueryKey.post(id), () => postApis.getPost(id), options as any ); };
이렇게 useGetPost 라는 react-query 의 커스텀 훅을 추가해주었다.
 

동적 라우팅 구현하기

notion image
이미지에서 pages/post/[id].tsx 라는 부분을 보면 조금 특이한 방식으로 파일을 생성했다. 이렇게 [id].tsx 파일을 생성해주면 클라이언트에서 접속시 /post/:id 와 같은 패턴으로 사용이 가능하다.
 
// pages/post/[id].tsx import Post from '../../components/Post'; import { postApis } from '../../core/apis/posts'; import type { GetServerSideProps } from 'next'; import type { ExtendedRecordMap } from 'notion-types'; import type { PageObjectResponse } from '@notionhq/client/build/src/api-endpoints'; import type { Response } from '../../interfaces'; import 'react-notion-x/src/styles.css'; import 'prismjs/themes/prism-tomorrow.css'; import 'katex/dist/katex.min.css'; interface IPostPageProps { id: string; data: Response<{ notionPage: ExtendedRecordMap; post: PageObjectResponse; }>; } export default function ({ id, data }: IPostPageProps) { return <Post id={id} data={data} />; } export const getServerSideProps: GetServerSideProps = async (context) => { const id = context.params?.id?.slice(1) as string; if (!id) { return { notFound: true }; } try { const res = await postApis.getPost(id); return { props: { id, data: res } }; } catch (e) { return { notFound: true }; } };
구현된 페이지는 아래와 같다. getServerSideProps 에서 처럼 idcontext 라는 파라미터에서 추출하여 사용할 수 있다. 이 id 로 포스트 목록을 만든것과 같은 방식으로 서버단계에서 미리 데이터를 페칭한다. 그리고 <Post /> 컴포넌트에 데이터를 주입해준다.
 

포스트 화면 구현하기

// components/Post/Post.tsx import dynamic from 'next/dynamic'; import { NotionRenderer } from 'react-notion-x'; import useRootState from '../../core/hooks/useRootState'; import type { ExtendedRecordMap } from 'notion-types'; import { PageObjectResponse } from '@notionhq/client/build/src/api-endpoints'; import { useGetPost } from '../../core/queries/posts'; import type { Response } from '../../interfaces'; interface IPostProps { className?: string; id: string; data: Response<{ notionPage: ExtendedRecordMap; post: PageObjectResponse; }>; } const Code = dynamic(() => import('react-notion-x/build/third-party/code').then((m) => m.Code) ); const Post: React.FC<IPostProps> = ({ id, data, className }) => { const { mode } = useRootState((state) => state.theme); const { data: postData } = useGetPost(id, { initialData: data }); return ( <div className={`post-wrapper ${className}`}> {postData?.data.notionPage && ( <NotionRenderer recordMap={postData.data.notionPage} darkMode={mode === 'dark'} components={{ Code }} /> )} </div> ); }; export default Post;
NotionRenderer 를 이용하여 가져온 notionPage 데이터를 넣어주면 자동으로 렌더하게 된다.
 
const Code = dynamic(() => import('react-notion-x/build/third-party/code').then((m) => m.Code) ); // ... // ... <NotionRenderer // ... components={{ Code }} />
이 부분을 추가하여야 코드블럭까지 정상적으로 렌더된다.
이렇게 하면 블로그의 전체적인 툴은 완성되었다. 이 NotionRenderer 를 이용해서 포스트 페이지는 날먹으로 완성이 되었다.
notion image
완성된 포스트 페이지의 모습
완성된 포스트 페이지의 모습
 

마무리

이제는 기본적인 블로그의 모습을 보이고 있다. 3일동안 짬날때 마다 작업한거 치고는 꽤 만족스러운 결과물이다.