노션 API 로 좋아요 구현하기노션 API 로 좋아요 구현하기

노션 API 로 좋아요 구현하기

2024-12-02

0

0

오늘의 개발 - 2024년 12월 1일

FEAT: 게시글 내 고정되는 댓글 섹션 구현

🚀 오늘의 목표

게시글 좋아요 실시간 반영 기능 개발
이미 좋아요를 눌렀을시에 블로킹 기능 개발
좋아요 취소 기능 개발

📝 개발 작업 내역

이미 게시글에 좋아요 기능이 존재했지만, 제대로 동작하지 않았다. 사실 게시글은 페이지 진입 시에 처음에만 불러왔기 때문에 좋아요 개수를 노출하면 좋아요 버튼이 눌렸을 때 실시간으로 반영되지 않을 것을 알기 때문에 개수 자체를 노출하지 않았다.
notion image
이런식으로 좋아요가 눌리면 내가 좋아요를 눌렀는지 확인이 가능해야 하고 좋아요 개수 또한 바로 반영이 되어야했다. 하지만 문제가 존재했던게 이런식의 빠른 반영이 이뤄지려면 빠른 API 속도가 보장이 되어야하고, 서버에서 어떻게 좋아요 여부를 저장할것인지에 대해서 고민이 필요했다.
 
기존에 떠올랐던 방안들은 이렇게 되었었는데
  • 사용자의 쿠키에 좋아요를 누른 포스트 ID 를 저장해서 여부 식별
  • 좋아요를 담당하는 노션 페이지를 따로 생성해서 관리 (…)
여러 방법이 더 존재하지만 이 이상은 오버엔지니어링이라 판단해서 더이상 생각하려 하지 않았다. 정말 당연하게도 두번째 방법을 택하면 좋아요 취소와 좋아요 요청시 굉장히 속도가 느릴것이기 때문에 조금이라도 성능을 올리고자 포스트의 좋아요 업데이트에만 API 를 사용하고 그 외의 것은 서버사이드에서 핸들링 하도록 정했다. 💪🏻

🐞 문제 및 해결 과정

좋아요를 어떻게 구현해야할까? 🤔
단순히 포스트 페이지에 좋아요 프로퍼티를 하나 생성하면 된다. Number 속성을 갖도록 구현 후 좋아요 버튼이 눌리면 API 요청 → 좋아요 + 1 → 성공 여부 반환 이런 방식으로 흘러가도록 하면 된다.
하지만 이런 상황에서의 문제가 있는데, 노션 API 는 속도가 굉장히 느리다. RDB 처럼 바로바로 CRUD 요청을 빠르게 실행할 수 없기때문에 각각의 작업 하나하나가 큰 성능 저하의 원인이 된다. (많이 요청하면 요청 한도에 걸릴 확률도 높기 때문에 주의가 필요하다. 💀)
실제로 한번의 요청에 약 3초가 걸린다.. 😭
실제로 한번의 요청에 약 3초가 걸린다.. 😭
따라서 나는 이미 받아온 게시글의 좋아요 수를 데이터에 담아 해당 값에서 +1 을 하도록 구현했다. 하지만 이런 방식으로 구현을 하게되면 이러한 문제가 발생 할 가능성이 있다. 💁🏻‍♂️
동시성 이슈와 비슷한데, 만약 두 사람이 동시에 내 글을 좋아요를 누른다면, 좋아요는 2개가 늘어나는게 아니라 1개만 늘어나게 된다. 보통의 커뮤니티나 이러한 작업이 빈번하게 일어나는 서비스는 모두 대응이 되어야겠지만, 한번의 디비 접근으로 여러 작업을 실행할 수 있는 환경이 아니었기 때문에 글쓴이는 단순 개인 블로그인점과, 좋아요 요청마다 API 를 2번 호출하는것은 불필요하다 생각했다.
 

쿠키를 다뤄보자 🍪
쿠키는 Next.js 환경에서 서버사이드에서만 접근이 가능하다. 기본적으로 쿠키 자체를 보안 관점에서 https 요청에서만 접근할 수 있도록 하기때문에 (물론 옵션으로 클라에서 접근 가능하도록 할 순 있다.) API ROUTE 를 하나 정의하고, 저장된 쿠키 중 PAGE-ID 를 갖고있다면 에러를 반환하도록 하는것이 적절하다 생각했다. (조금이라도 API 호출을 줄일 수 있는 방안으로..)
 
export const getPostLikeStatus = async (pageId: string): Promise<boolean> => { try { const likedPosts = parseLikedPosts(cookies().get(LIKED_POSTS_COOKIE_NAME)?.value); return likedPosts.includes(pageId); } catch (error) { console.error('쿠키 접근 중 오류 발생:', error); return false; } }; export const savePostLikeStatus = async (pageId: string, isLiked: boolean): Promise<void> => { try { const cookieStore = cookies(); const likedPosts = parseLikedPosts(cookieStore.get(LIKED_POSTS_COOKIE_NAME)?.value); if (isLiked) { if (!likedPosts.includes(pageId)) { likedPosts.push(pageId); } } else { const index = likedPosts.indexOf(pageId); if (index !== -1) { likedPosts.splice(index, 1); } } cookieStore.set(LIKED_POSTS_COOKIE_NAME, JSON.stringify(likedPosts), cookieOptions); } catch (error) { console.error('쿠키 저장 중 오류 발생:', error); } };
자료구조로는 Map 이 나을 수 있겠다는 생각도 있다.🤨
 
이런식으로 쿠키를 사용하여 좋아요 여부를 배열로 관리해서 관리할 수 있도록 구현해주었다. 그리고 POST API 를 구현하기 위해 라우터를 하나 정의하고, 데이터로는 업데이트와 쿠키 상태 업데이트를 위한 pageId 와 좋아요 개수인 count 를 받도록 했다.
 
// /api/likes/route.ts export async function POST(req: NextRequest) { try { const data = await req.json(); const { pageId, count } = data; if (!pageId) { return NextResponse.json( { error: '페이지 ID 나 요청값이 존재하지 않습니다.' }, { status: 400 } ); } await Promise.all([ notionClient.updatePostLike(pageId, count + 1), savePostLikeStatus(pageId, true), ]); return NextResponse.json({ status: 201 }); } catch (error) { console.error('Error post Like:', error); return NextResponse.json({ error: '좋아요에 실패하였습니다.' }, { status: 500 }); } } // /lib/cookies.ts async function updatePostLike(pageId: string, count: number) { try { await notion.pages.update({ page_id: pageId, properties: { Likes: { type: 'number', number: count, }, }, }); } catch (error) { console.error('Error liking post:', error); } }
Promise.all 을 이용해 병렬로 실행하도록 해서 조금이라도 빨라지길 기도하면서 구현했다.
 

좋아요를 구현했는데.. 동기화 문제가 발생한다. 🥺
이건 어느 프레임워크를 사용해도 마찬가지 일 것 같은데, 보통 좋아요 기능을 구현하기 위해서 고민해야 할 부분들이 많다. 단순히 게시글 엔티티에 좋아요를 의미하는 데이터를 정의하고 그 값으로 모든걸 해결하려고 하면 문제가 발생한다. 왜냐하면 이렇게 실시간으로 변경되는 데이터가 정적이면 클라이언트에서 모든것을 해결해야 하기 때문이다.
  1. 좋아요를 한번에 각각 다른곳에서 두번 이상 동시에 실행되었을 때
  1. 좋아요 수가 다른 페이지에서도 노출되어야 하고 실시간 좋아요 수를 반영해야 할 때
이러한 행동들을 리액트 쿼리로 구현하게 된다면 지옥이 펼쳐지게 된다. 😈 바로 비동기 로직을 실행함으로서 얻는 데이터가 아닌 쿼리캐시를 직접 변경해야 하거나 이미 서버데이터로만 사용하는 값을 로컬상태로 다시 저장해서 값을 표현하거나 해야하는 문제가 발생하기 때문에 부가적으로 고민해야 하는 부분이 많아진다.
 
사실 이러한 부분까지 고려해야 하나 싶기도 하고, 블로그 특성상 여러 사람들이 한번에 내 포스트를 좋아요 하거나 할 일이 없을거기 때문에 (..) 낙관적 업데이트를 통해 내가 보는 화면에서만 데이터를 업데이트 한 것 처럼 보이게 해버려도 괜찮지만, 정확한 값과 마찬가지로 서버데이터를 임의로 변경하거나 타입문제로 고생하기 싫었기 때문에 API를 하나 더 정의해주었다.
 
export async function GET(req: NextRequest) { try { // URL에서 pageId 추출 const url = new URL(req.url); const pageId = url.searchParams.get('pageId'); if (!pageId) { return NextResponse.json( { error: '페이지 ID 나 요청값이 존재하지 않습니다.' }, { status: 400 } ); } const isLiked = await getPostLikeStatus(pageId); const likeCount = await notionClient.getPostLikeCount(pageId); return NextResponse.json({ isLiked, likeCount }); } catch (error) { console.error('Error post Like:', error); return NextResponse.json({ error: '좋아요에 실패하였습니다.' }, { status: 500 }); } }
이렇게 정의하면 좋아요 이후에 좋아요 개수를 최신화 하기 용이하지만, 한번의 요청에 2개의 API를 호출하게 된다. 😇
 
export const useLikePost = ( request: PostLikeRequest, option?: UseMutationOptions, ) => { const queryClient = useQueryClient(); return useMutation({ mutationFn: () => postService.postLikePage(request), ...option, onSuccess: (...args) => { option?.onSuccess?.(...args); queryClient.invalidateQueries({ queryKey: PostQueryKeys.getPostLike(request.pageId) }); }, onError: (...args) => { option?.onError?.(...args); }, }); };
그리고 이런식으로 공용 모듈로 사용할 mutation 훅을 정의해주고 좋아요 버튼의 이벤트 핸들러 내에 mutation 을 호출하도록 하면 중복 정의와 불필요한 고민 없이 좋아요 기능을 구현 할 수 있다.
 

🌟 오늘의 성과

사용성도 생각했지만, 무엇보다 현재 배포 환경을 고려한 개발을 진행하지 않았을까? UX적인 측면을 생각해서 다음 작업으로는 낙관적 업데이트를 적용해볼 생각이다. 나에게 있어 좋아요는 크게 중요하지 않기때문에 좋아요를 눌렀는데 스피너만 몇 초 동안 발생하는 상황보다는 아무래도 즉각적인 업데이트가 보여야지 독자 입장에서 좋을 것 같다는 생각이 든다.
 

📅 다음 작업 계획

게시글 시리즈

💡 추가 메모

자유롭게 기록할 수 있는 공간 (개인적인 인사이트, 아이디어 등)
 

BRIN</>E

© 2025