노션 블로그 만들기 - Notion-API 응답시간 개선기 - 1
도대체 뭐가 문제기에 11초나 응답이 걸리는 거야
2023-03-01 | 프로젝트 | 8min
현재 요청 응답 시간 지표...
- 현재 요청하는 방식은
api/post
디렉토리의index.ts
파일에서 notion-api를 이용하는 라이브러리를 활용하여 요청을 보내는 방식을 이용하고 있다. - 문제는 이게 시간이 너무 오래 걸린다는 것이다
- 위 그림을 보면 서버 응답까지 11.76초 → 애초에 placeholder이나 스켈레톤 컴포넌트를 설정하지 않았다고 하더라도 이는 너무나 큰 수치다
React-Query의 이용
- 우선 fetching 한 데이터를 caching 해놓는게 좋을 것 같았다. 이럼 초기 컨텐츠 로딩 이후 재 접속시 caching된 데이터를 이용하기에 딜레이가 매우 개선되는 것을 확인할 수 있었다.
- 근데 정작 본 문제는 해결되지 않는다.
여기서 ChatGPT의 의견을 물어보았고 얻은 대답은 Next.js의 server side rendering을 이용하는 것이었다
지금까지의 fetching 전략
현재 코드를 살펴보면,
export default function SinglePost({ imageProps }: any) {
const router = useRouter();
const postId = router.query.id as string;
const { isLoading, isSuccess, isError, data, error } = useQuery(
['single-post', router.query.id],
() => fetchSinglePost(postId),
{ staleTime: 3000 }
);
return (
<ArticleLayout>
<section css={sectionStyle}>
<h1>{post && post.title}</h1>
<p>{post && post.date}</p>
{post && (
<Image
css={imgStyle}
src={post.imgLink as string}
alt={post.title}
width={700}
height={430}
/>
)}
{post && <Markdown content={post.content} />}
</section>
</ArticleLayout>
);
}
의 방식이다. 물론 react-query를 이용하기에 onError
시 로직과, onLoading
시 로직이 둘 다 있긴 하지만 생략했다.
- 해당 기능 이용하면서 생기는 아래 이슈를 조심하자
React - React Query 와 setState
개선 전략 시도 : 서버 사이드 렌더링하기
Nextjs의 가장 강력한 기능을 드디어 사용해본다
우선 getServerSideProps
의 형태로 데이터를 불러와볼 것이다. Next13의 새롭게 생긴 use 방식은 나중에 생각헤보자.
export const getServerSideProps = async ({
params,
}: GetServerSidePropsContext): Promise<{ props: any; revalidate: number }> => {
const postData = await fetchSinglePost(params?.id as string);
return {
props: postData,
revalidate: 1,
};
};
위와 같은 형식으로 가져와봤다.
결과는 똑같이 오래 걸리게 되는 것이다.
생각해보면 그럴만한게, 위를 보는 fetchSinglePost
함수를 사용하고 있다. 그런데 해당 코드가 구현된 모습을 보면,
export const fetchSinglePost = async (id: string) => {
const post = await axios.get(`${server}/api/posts/${id}`);
return post.data;
};
위와 같이 구현되어 있다. 즉, cors이슈를 피하기 위해 api route에 추가해놓은 주소로 요청을 보냈던 것이다.
애초에 server side에서 렌더링이 이루어지기에 이는 cors 이슈를 걱정할 필요가 없다. 그냥 순수한 주소로 바로 요청을 보내도 상관 없다.
추가로, getServerSideProps
도 좋지만, 한번 publish 한 뒤의 컨텐츠의 변화 주기가 그리 크기 않을 것 같아서 getStaticProps
을 이용해보기로 했다.
getStaticProps
코드는 위와 유사하다. 문제는 getStaticProps
의 경우에는 getStaticPath
를 설정해주어야 한다는 것이다.
invalid-getstaticpaths-value | Next.js
그렇지 않으면 위와 같은 이슈가 발생하게 된다.
추가로 getStaticProps
메소드 내에서 데이터를 페칭하기 위해 라이브러리를 import한다면, nextjs에서는 해당 라이브러리를 client side의 번들에서 이를 제외하게 된다. 즉, 이는 번들 크기에서도, 데이터 보안에서도 모두 이점을 가져갈 수 있는 방식이다.
// Functions for getStaticPath
export const getAllDatabaseIDs = async () => {
let databaseIDs: string[] = [];
const rootBlockChildren = await notion.blocks.children.list({
block_id: rootNotionPageId,
});
rootBlockChildren.results.map(
(database: BlockObjectResponse | PartialBlockObjectResponse) => {
const DB = database as BlockObjectResponse;
if (DB.type === 'child_database') {
databaseIDs.push(DB.id);
}
}
);
return databaseIDs;
};
export const getAllPostIDs = async () => {
const databaseIDs = await getAllDatabaseIDs();
let postIDs: string[] = [];
databaseIDs.map(async (databaseID) => {
const posts = await getDatabasePostIds(databaseID);
posts.map((post: PageObjectResponse | PartialPageObjectResponse) => {
const page = post as PageObjectResponse;
const id = page.id;
postIDs.push(id);
});
});
return postIDs;
};
위와 같은 방식으로 작성한 모든 데이터베이스 내 포스트들의 id들을 가져와서
export const getStaticPaths = async () => {
const postIds = await getAllPostIDs();
const paths = postIds.map((postId) => {
return { params: { postId: postId } };
});
return {
paths: paths,
fallback: true,
};
};
export const getStaticProps = async ({
params,
}: GetStaticPropsContext): Promise<{ props: any; revalidate: number }> => {
const postData = await fetchSingleStaticPost(params?.id as string);
return {
props: postData,
revalidate: 3600,
};
};
이렇게 작성하면 Static 한 페이지에 대한 path들을 모두 getStaticPaths
를 통해 기록한 뒤 해당 id로 fetch를 진행하면 개선되지 않을까? 라는 생각에 시도해보았다. 결과는…?
헉. 더걸린다… 왜지…?
아무래도 getStaticProps
을 통해서 가져온 정보가 클라이언트에 오자마자 다시 refetching되는 시간까지 포함돼서 가는 것 같다
그런데 왜 다시 요청이 가는거지?
렌더링 이후 실행되는 기존의 코드는 이렇다
const { isLoading, isSuccess, isError, data, error } = useQuery(
['single-post', router.query.id],
() => fetchSinglePost(postId),
{ staleTime: 3000 }
);
그런데 getStaticProps
를 이용한 뒤 useState
와 useQuery
를 통해서 상태관리를 해주려고 했더니
const [post, setPost] = useState<PostContentType | undefined>(undefined);
const { isLoading, isSuccess, isError, data, error } = useQuery(
['single-post', router.query.id],
() => fetchSinglePost(postId),
{ staleTime: 3000, initialData: postData }
);
useEffect(() => {
if (data) {
setPost(data);
}
}, [data]);
위 처럼 작성하니
아래와 같이 결과가 나왔다.
- 위는
SSR
시 data fetching에 걸린 시간을, - 아래는
useEffect
문에 의해 re-rendering 된 결과라고 할 수 있을 것 같다
걸린 시간은 차례대로 7초, 11초 정도이다.
생각해보니 SSR 페이지를 받기 위해서 서버 단에서 페이지 생성시 필요한 정보 요청 한번과 브라우저에서 해당 정보를 컴포넌트 로딩시 다시 요청을 하는 방식이기에 이런 현상이 발생하는 것 같았다.
그래서 아무래도 서버에서 시간이 걸리는 것이든, 브라우저에서 시간이 걸리는 것이든 근본적인 서버 응답 시간을 줄이는 방법 자체를 생각을 해보았다.
백엔드 api 통신 과정
기존의 코드는 아래와 같다.
export const getPostImage = async (postId: string) => {
const page = await notion.pages.retrieve({ page_id: postId });
return page.cover.external.url as string;
};
export const getPostTitle = async (postId: string) => {
const page = await notion.pages.retrieve({ page_id: postId });
return page.properties.Title.title[0].plain_text;
};
export const getPostDate = async (postId: string) => {
const page = await notion.pages.retrieve({ page_id: postId });
return page.properties.Date.date.start;
};
export const getPostTags = async (postId: string) => {
const page = await notion.pages.retrieve({ page_id: postId });
return page.properties.Date.date.start;
};
export const getPostContent = async (postId: string) => {
const mdBlocks = await n2m.pageToMarkdown(postId);
return n2m.toMarkdownString(mdBlocks);
};
각각은 하나의 페이지의 id를 가지고서 해당 페이지에 대한 이미지 url, 제목, 날짜, 태그, 본문 내용을 가져온다. 그리고 해당 요청들은 아래와 같이 한꺼번에 이용된다.
export const fetchSingleStaticPost = async (id: string) => {
const postContent = await getPostContent(id);
const postTitle = await getPostTitle(id);
const postDate = await getPostDate(id);
const postImage = await getPostImage(id);
const postTags = await getPostTags(id);
return {
imgLink: postImage,
content: postContent,
id: id as string,
title: postTitle,
date: postDate,
tags: postTags,
};
};
아무래도 이쪽 원인이 젤 큰 것 같다. 비동기 처리의 장점을 오히려 동기 처리로 만들어 버리는 바람에 시간이 오래 걸리게 된 것인가? 이를 한번 아래와 같이 바꾸어서 진행을 해보았다.
다시다시, 아래와 같이 await Promise.all
을 통해 구현했다.
export default async function postIdHandler(
req: NextApiRequest,
res: NextApiResponse<PostContentType>
) {
const { postId } = await req.query;
const [postContent, postTitle, postDate, postImage, postTags] =
await Promise.all([
getPostContent(postId as string),
getPostTitle(postId as string),
getPostDate(postId as string),
getPostImage(postId as string),
getPostTags(postId as string),
]);
const post = {
postId,
postTitle,
postDate,
postImage,
postTags,
postContent,
};
res.status(200).send({
imgLink: postImage,
content: postContent,
id: postId as string,
title: postTitle,
date: postDate,
tags: postTags,
});
}
그런데도 사실상 DB 응답 시간에 대한 개선은 이루어지지 않았다. 1초 이내로 빨라졌을 뿐이다. 사실 그보다도 적다.
결론
이 길고 긴 여정에서 얻은 결과는 결국 노션 DB를 통해서 정보를 가져오는 것 자체가 오래걸리는 것이었다.
다시 생각해보면 당연하다. 결국 요청을 한 번 하게 되면,
- “크롬이 → 노션 디비로 요청 → 노션 디비 → 다시 크롬“
위와 같은 과정이 이뤄지는데 중간에 노션 디비 요청에서 노션 디비를, 이를 다시 크롬이 가져오는 것 자체가 너무나도 오래 걸리게 된다.
사실 아직까지도 왜 그런지 정확히 이해가 되지 않는다.
- 브라우저에서 노션 디비의 정보를 가져오는 것에는 왜 순수 노션 앱에서 컨텐츠를 가져오는 것과 속도가 다른지?가 궁금해진다. 어떤 점에서 차이가 있는 것일까…?
그래도 여기서 다양한 것들을 얻게 되었다.
- React Query를 이용한 캐싱 기능의 발견
- 추가로 pre-fetching 기능이 있어 로딩 속도를 개선할 수 있긴 한데 이를 적용하는 것보다 애초의 디비 응답 속도를 늘리는 것이 우선순위라고 생각되었다.
- Next.js의 SSR 경험
- SSR을 쓰면 단순하게 “빠르게 로딩된다” 라는 점이 이제는 다르게 느껴진다. 오히려 SSR은 로딩속도가 느리다는 것이 내 생각이다. 하지만, 서버에서 페이지 생성 시 이미 컨텐츠가 담겨진 채로 오기에 사용자의 입장에서는 컨텐츠를 볼 때, 딜레이 없이 그것이 채워진 채로 컨텐츠를 접한다는 점에서 UX를 해치지 않는다는 것이 장점이라고 봐진다.
- 또한 SSR의 적용은 DB의 응답속도가 느린경우 오히려 쥐약임을 느끼게 되었다. → 11초 걸리던 응답속도가 서버로 넘어가도 컨텐츠를 가져오는데 시간이 7초면 오히려 가장 오래 걸리던 DB를 바꾸는 것이 났다고 느꼈다.