포스트 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](https://www.notion.so/image/https%3A%2F%2Ffile.notion.so%2Ff%2Ff%2F3b78fcca-37ce-4b3e-946b-08dd42e690b1%2Fa4761ff7-467b-490b-83d7-1ad5907a04e7%2FUntitled.png%3Fid%3D040d9a8f-59bf-450f-a6d9-0e86a933291e%26table%3Dblock%26spaceId%3D3b78fcca-37ce-4b3e-946b-08dd42e690b1%26expirationTimestamp%3D1721822400000%26signature%3DUkO40IFzzCQQ2jI0tG_FyJ7lhVyJsBNtV8lxOVwBJec?table=block&id=040d9a8f-59bf-450f-a6d9-0e86a933291e&cache=v2)
이미지에서
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
에서 처럼 id
를 context
라는 파라미터에서 추출하여 사용할 수 있다. 이 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](https://www.notion.so/image/https%3A%2F%2Ffile.notion.so%2Ff%2Ff%2F3b78fcca-37ce-4b3e-946b-08dd42e690b1%2F3128b486-0cd7-4d5a-b23e-c53062c44dd6%2FUntitled.png%3Fid%3Db2ae6ff9-e848-44bd-8b03-285306c79875%26table%3Dblock%26spaceId%3D3b78fcca-37ce-4b3e-946b-08dd42e690b1%26expirationTimestamp%3D1721822400000%26signature%3DX7oeRlgfK5o_R1TBYoQ6TB26DX7NXEXqQnQXXoR6r3o?table=block&id=b2ae6ff9-e848-44bd-8b03-285306c79875&cache=v2)
![완성된 포스트 페이지의 모습](https://www.notion.so/image/https%3A%2F%2Ffile.notion.so%2Ff%2Ff%2F3b78fcca-37ce-4b3e-946b-08dd42e690b1%2F03757559-9905-47d0-974a-3ab978224912%2FUntitled.png%3Fid%3D21826038-4f09-491f-878b-e328d24ca9f3%26table%3Dblock%26spaceId%3D3b78fcca-37ce-4b3e-946b-08dd42e690b1%26expirationTimestamp%3D1721822400000%26signature%3DKlNH4Y6CzHVJ92FfPBkRqrHLHQMzd1hc1VtlOp2E38Y?table=block&id=21826038-4f09-491f-878b-e328d24ca9f3&cache=v2)
마무리
이제는 기본적인 블로그의 모습을 보이고 있다. 3일동안 짬날때 마다 작업한거 치고는 꽤 만족스러운 결과물이다.