Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: 모든 API 호출 로직이 codegen을 통하도록 수정함 #226

Merged
merged 14 commits into from
Jul 31, 2024
Merged
67 changes: 21 additions & 46 deletions apps/next-app/src/app/(hasGNB)/_components/Nav/Nav.tsx
Original file line number Diff line number Diff line change
@@ -1,59 +1,34 @@
import React, { forwardRef } from 'react'
import { cookies } from 'next/headers'
'use client'
import { usePathname, useRouter } from 'next/navigation'
import { forwardRef, Suspense } from 'react'

import ProfilePopover from 'components/common/Layout/Nav/ProfilePopover'
import { getApiEndpoint } from 'envs'
import * as S from 'components/common/Layout/Nav/Nav.style'
import { GoToSignUpButton } from './GoToSignUpButton'
import { Profile } from 'components/common/Layout/Nav/Profile'
import { ROUTE } from 'constants/route'
import { getAccessTokenFromCookie } from 'features/auth/token'
import { LogoButton } from './LogoButton'

async function getUserProfile() {
const cookieStore = cookies()
const accessToken = cookieStore.get('accessToken')
if (!accessToken) {
return null
}
const enableGoToSignUpButton = (pathname: string | null) =>
pathname === ROUTE.SIGN_UP || pathname === ROUTE.INTRODUCE

const res = await fetch(`${getApiEndpoint()}/users/me`, {
headers: {
Authorization: `Bearer ${accessToken.value}`,
},
})
// The return value is *not* serialized
// You can return Date, Map, Set, etc.

if (res.status !== 200) {
// This will activate the closest `error.js` Error Boundary
throw new Error('Failed to fetch data')
}

return res.json()
}

const Nav = forwardRef<HTMLDivElement>(async function Nav(props, ref) {
const userProfile = await getUserProfile()
// NOTE: 서버 컴포넌트로 만들면 클라 측에서 갱신이 안됨
const Nav = forwardRef<HTMLDivElement>(function Nav(props, ref) {
const pathname = usePathname()
const router = useRouter()

return (
<S.TopNavContainer ref={ref}>
<LogoButton />

{userProfile?.name ? (
<ProfilePopover>
<S.MyPageButton>
<S.UserName>{`${userProfile.name}님, 안녕하세요!`}</S.UserName>
{userProfile.profileImageUrl && (
<S.UserImage
width={32}
height={32}
alt="프로필 사진"
src={userProfile.profileImageUrl}
priority
/>
)}
</S.MyPageButton>
</ProfilePopover>
{enableGoToSignUpButton(pathname) ? (
<S.GoToSignUpButton onClick={() => router.push(ROUTE.SIGN_UP)}>
피둥 시작하기
</S.GoToSignUpButton>
) : (
<GoToSignUpButton />
getAccessTokenFromCookie() && (
<Suspense>
<Profile />
</Suspense>
)
)}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

조건이 이렇게 걸리면 피둥을 사용하지 않는(혹은 로그인하지 않은 유저)가 타인의 구독리스트를 보러 들어 갔을 때 오른쪽 상단에 아무 것도 노출되지 않게 되는데 의도하신걸까요? 👀 개인적으로는 타인의 구독 리스트를 보러 갔을때 우측 상단에 '피둥 시작하기' 버튼을 띄워주면 유입의 가능성이 있다고 생각해서 피둥시작하기 버튼 or 프로필 노출의 분기 조건을 로그인 여부로 사용하면 어떨까 하는 의견이에요

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@eunsonny 깜빡임이 있어서 저렇게 처리가 되었던 것인데 말씀해주신 케이스는 제가 놓쳤어요
적절한 조건을 찾아서 수정해볼게요

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

깜빡임의 원인을 찾았는데요 말씀하신 대로 로그인 시 보이도록 하되

  • 일단 AsyncLocalStorage를 쓰면 대강 구현을 시도해보고 실패하면
  • app router로 마이그레이션 완료되기 까지 Nav 내 유저 데이터 / 버튼 간 깜빡임 이슈는 known issue로 두고 빠르게 나머지 경로도 app router로 마이그레이션 했으면 해요.

핵심은 아래 함수인데요

const getIsomorphicCookies = () =>
  isServer() && isAppRouter() ? getNextCookies() : Cookies

여기에서 앱 라우터 내 로직이면 getNextCookies를 타면서 cookie를 불러오고, 페이지 라우터 & 클라이언트 로직이면 js-cookie의 Cookies 로직을 타도록 되어 있어요.

근데 제가 놓지고 있던게 js-cookie는 서버 측에서는 기본적으로 동작하질 않아요
https://github.com/js-cookie/js-cookie/blob/main/src/api.mjs#L4-L8

function init(converter, defaultAttributes) {
  function set(name, value, attributes) {
    if (typeof document === 'undefined') {
      return
    }

깔려 있지만 사용되지 않고 있는 nookies라는 라이브러리를 사용하면 동작하긴 하는데 그러려면 getServerSideProps에서 ctx 객체에 접근해 cookie 값을 읽고 내려줘야 하는데... 현재 코드 구조 상 이걸 전역적으로 주고 받을 수가 없어서(app router에서는 전역적으로 주고 받기 위한 구현이 추가되었어요) 쿠키 관련 로직 사용하기가 좀 곤란해요.

이 문제를 해결하기 위해 서버 측에서 AsyncLocalStoarge를 사용하도록 하거나 잠시 흐린 눈 하는 방법을 선택해야 할거 같아요

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아하 이해했습니당

  • 일단 AsyncLocalStorage를 쓰면 대강 구현을 시도해보고 실패하면
  • Nav 내 유저 데이터 / 버튼 간 깜빡임 이슈는 known issue로 두고 빠르게 나머지 경로도 app router로 마이그레이션
    이 방향으로 진행하는 것으로 알고 있겠습니다. 저는 app router로 빠르게 마이그레이션에 힘을 보탤게요(...)

</S.TopNavContainer>
)
Expand Down
20 changes: 6 additions & 14 deletions apps/next-app/src/app/(hasGNB)/feed/me/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,15 @@ import type { NextPage } from 'next'
import { cookies } from 'next/headers'
import { Suspense } from 'react'

import { itemQueries } from 'entities/item/api'
import MyFeed from 'views/myFeed'
import { SkeletonPostType } from 'components/common/Skeleton'
import { getQueryClient } from 'core/getQueryClient'
import { feedoongApi } from 'services/api'
import { setAuthorizationHeader } from 'features/auth/token'
import { itemQueries } from 'entities/item/api'
import { checkLoggedIn } from 'shared/utils/checkLoggedIn'
import { SkeletonPostType } from 'components/common/Skeleton'

const FeedMePage: NextPage = () => {
const api = feedoongApi()
const cookieStore = cookies()
const accessToken = `${cookieStore.get('accessToken')?.value}`

setAuthorizationHeader(api, accessToken, { type: 'Bearer' })
import MyFeed from 'views/myFeed'

const FeedMePage: NextPage = async () => {
const queryClient = getQueryClient()
void queryClient.prefetchInfiniteQuery(itemQueries.list(api))
await queryClient.prefetchInfiniteQuery(itemQueries.list())

return (
<HydrationBoundary state={dehydrate(queryClient)}>
Expand All @@ -28,7 +20,7 @@ const FeedMePage: NextPage = () => {
<SkeletonPostType key={index} />
))}
>
<MyFeed isLoggedIn={checkLoggedIn(cookieStore)} />
<MyFeed isLoggedIn={checkLoggedIn(cookies())} />
</Suspense>
</HydrationBoundary>
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ export const Container = styled.div`
gap: 12px;
border-radius: 20px;
border-bottom-left-radius: 0px;
margin-bottom: 20px;
`

export const Title = styled.h2`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ export const PrivatePostType = ({ item }: { item: PrivatePost }) => {
src={item.isLiked ? Icons.Bookmark : Icons.BookmarkDeactive}
width={16}
height={16}
onClick={() => handleLike(String(item.id))}
onClick={() => handleLike(item.id)}
priority
/>
</CardActions>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { useMutation, useQueryClient } from '@tanstack/react-query'
import produce from 'immer'

import { CACHE_KEYS } from 'services/cacheKeys'
import { submitViewedPost } from 'services/feeds'
import { viewItemUsingPOST } from 'services/types/_generated/item'
import type { Feed, SubmitViewedPost } from 'types/feeds'
import { mergeObjectsByMutate } from 'utils/common'

Expand All @@ -17,7 +17,7 @@ const useReadPost = (item: { id: number }) => {

const { mutate: handleRead } = useMutation({
mutationKey: CACHE_KEYS.viewItem(item.id),
mutationFn: submitViewedPost,
mutationFn: viewItemUsingPOST,
onSuccess: (data, variables) => {
client.setQueryData<PrevDataType>(CACHE_KEYS.feeds, (prev) => {
if (!prev) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,18 @@ import { useMutation, useQueryClient } from '@tanstack/react-query'
import Toast from 'components/common/Toast'
import { itemQueries } from 'entities/item/api'
import { CACHE_KEYS } from 'services/cacheKeys'
import { likePost, unlikePost } from 'services/feeds'
import {
likeUsingPOST,
unlikeUsingDELETE,
} from 'services/types/_generated/like'

// TODO: 추후에 useToggleLike 자체를 재작성해야 함. 임시로 인자 타입 변경
const useToggleLike = (item: { id: number; isLiked: boolean }) => {
const client = useQueryClient()

const { mutate: handleLike } = useMutation({
mutationKey: CACHE_KEYS.likePost(item.id),
mutationFn: !item.isLiked ? likePost : unlikePost,
mutationFn: !item.isLiked ? likeUsingPOST : unlikeUsingDELETE,
onSuccess: async (data) => {
client.invalidateQueries(itemQueries.list())
client.invalidateQueries({ queryKey: CACHE_KEYS.feeds })
Expand Down
39 changes: 16 additions & 23 deletions apps/next-app/src/components/common/Layout/Nav/Nav.tsx
Original file line number Diff line number Diff line change
@@ -1,45 +1,38 @@
'use client'

import React, { forwardRef } from 'react'
import { useRouter } from 'next/navigation'
import { usePathname, useRouter } from 'next/navigation'
import { forwardRef, Suspense } from 'react'

import { useGetUserProfile } from 'features/user/userProfile'
import ProfilePopover from './ProfilePopover'
import { ROUTE } from 'constants/route'
import LogoDesktopNoBackground from 'components/common/LogoDesktop'
import { ROUTE } from 'constants/route'
import { getAccessTokenFromCookie } from 'features/auth/token'
import { Profile } from './Profile'

import * as S from './Nav.style'

const enableGoToSignUpButton = (pathname: string | null) =>
pathname === ROUTE.SIGN_UP || pathname === ROUTE.INTRODUCE

const Nav = forwardRef<HTMLDivElement>(function TopNavBar(props, ref) {
const router = useRouter()
const { data: userProfile } = useGetUserProfile()
const pathname = usePathname()

return (
<S.TopNavContainer ref={ref}>
<S.LogoButton onClick={() => router.push('/')}>
<LogoDesktopNoBackground color={'var(--color-black)'} />
<S.Feedoong>Feedoong</S.Feedoong>
</S.LogoButton>

{userProfile?.name ? (
<ProfilePopover>
<S.MyPageButton>
<S.UserName>{`${userProfile.name}님, 안녕하세요!`}</S.UserName>
{userProfile.profileImageUrl && (
<S.UserImage
width={32}
height={32}
alt="프로필 사진"
src={userProfile.profileImageUrl}
priority
/>
)}
</S.MyPageButton>
</ProfilePopover>
) : (
{enableGoToSignUpButton(pathname) ? (
<S.GoToSignUpButton onClick={() => router.push(ROUTE.SIGN_UP)}>
피둥 시작하기
</S.GoToSignUpButton>
) : (
getAccessTokenFromCookie() && (
<Suspense>
<Profile />
</Suspense>
)
)}
</S.TopNavContainer>
)
Expand Down
32 changes: 32 additions & 0 deletions apps/next-app/src/components/common/Layout/Nav/Profile.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
'use client'
import { useSuspenseQuery } from '@tanstack/react-query'

import { CACHE_KEYS } from 'services/cacheKeys'
import { getUserInfoUsingGET } from 'services/types/_generated/user'
import ProfilePopover from './ProfilePopover'

import * as S from './Nav.style'

export const Profile = () => {
const { data: userProfile } = useSuspenseQuery({
queryKey: CACHE_KEYS.me,
queryFn: getUserInfoUsingGET,
})

return (
<ProfilePopover>
<S.MyPageButton>
<S.UserName>{`${userProfile.name}님, 안녕하세요!`}</S.UserName>
{userProfile.profileImageUrl && (
<S.UserImage
width={32}
height={32}
alt="프로필 사진"
src={userProfile.profileImageUrl}
priority
/>
)}
</S.MyPageButton>
</ProfilePopover>
)
}
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
import { useState } from 'react'
import { useQuery } from '@tanstack/react-query'
import { useRouter } from 'next/router'
import { useState } from 'react'
import Skeleton from 'react-loading-skeleton'

// import * as S from 'components/views/MyPost/PostContainer.style'

import Flex from 'components/common/Flex'
import FeedItem from 'components/common/FeedItem/FeedItem'
import { getChannel } from 'services/feeds'
import { CACHE_KEYS } from 'services/cacheKeys'
import Flex from 'components/common/Flex'
import LogoIcon from 'components/common/LogoIcon'
import PageContainer from 'components/common/PageContainer'
import Paging from 'components/common/Paging'
import { SkeletonPostType } from 'components/common/Skeleton'
import { ITEMS_PER_PAGE } from 'constants/pagination'
import { CACHE_KEYS } from 'services/cacheKeys'
import { getItemsOfSubscribedChannelUsingGET } from 'services/types/_generated/item'
import { getWellKnownChannelImg } from 'utils'
import PageContainer from 'components/common/PageContainer'
import LogoIcon from 'components/common/LogoIcon'
import ChannelSubscription from './ChannalSubscription'

import * as S from './ChannelDetailContainer.style'
Expand All @@ -26,7 +26,11 @@ function PostContainer() {
const [currentPage, setCurrentPage] = useState(1)
const { data, isLoading } = useQuery({
queryKey: [CACHE_KEYS.likedPosts, { page: currentPage, channel: id }],
queryFn: () => getChannel(id, currentPage),
queryFn: () =>
getItemsOfSubscribedChannelUsingGET(Number(id), {
page: currentPage,
size: 10,
}),
enabled: !!id,
})

Expand Down
2 changes: 2 additions & 0 deletions apps/next-app/src/components/views/Feeds/FeedsContainer.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
'use client'

import { useRouter } from 'next/router'
import { SwitchCase } from '@toss/react'

Expand Down
13 changes: 9 additions & 4 deletions apps/next-app/src/components/views/Feeds/MyFeed/MyFeed.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,24 @@
'use client'
import { useInfiniteQuery } from '@tanstack/react-query'
import { useEffect } from 'react'
import { useInView } from 'react-intersection-observer'

import { SkeletonPostType } from 'components/common/Skeleton'
import { CACHE_KEYS } from 'services/cacheKeys'
import { getFeeds } from 'services/feeds'
import FeedItem from 'components/common/FeedItem'
import Loading from 'components/common/Loading'
import { SkeletonPostType } from 'components/common/Skeleton'
import { CACHE_KEYS } from 'services/cacheKeys'
import { getItemsUsingGET } from 'services/types/_generated/item'
import * as S from '../FeedsContainer.style'

const MyFeed = () => {
const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isFetching } =
useInfiniteQuery({
queryKey: CACHE_KEYS.feeds,
queryFn: ({ pageParam = 1 }) => getFeeds(pageParam),
queryFn: ({ pageParam = 1 }) =>
getItemsUsingGET({
page: pageParam,
size: 10,
}),
initialPageParam: 1,
staleTime: 1000 * 60 * 5,
getNextPageParam: (lastPage) =>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import { useQuery } from '@tanstack/react-query'

import FeedItem from 'components/common/FeedItem'
import { CACHE_KEYS } from 'services/cacheKeys'
import { getRecommendedChannels } from 'services/recommendations'
import { SkeletonChannelType } from 'components/common/Skeleton'
import { CACHE_KEYS } from 'services/cacheKeys'
import { getRecommendedChannelsUsingGET } from 'services/types/_generated/channel'
import * as S from '../FeedsContainer.style'

const RecommendedChannels = () => {
const { data, isFetching } = useQuery({
queryKey: CACHE_KEYS.recommended(['channels']),
queryFn: getRecommendedChannels,
queryFn: getRecommendedChannelsUsingGET,
})

const showSkeleton = isFetching && !data
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import { useQuery } from '@tanstack/react-query'

import FeedItem from 'components/common/FeedItem'
import { CACHE_KEYS } from 'services/cacheKeys'
import { getRecommendedPosts } from 'services/recommendations'
import { SkeletonPostType } from 'components/common/Skeleton'
import { CACHE_KEYS } from 'services/cacheKeys'
import { getRecommendedItemsUsingGET } from 'services/types/_generated/item'
import * as S from '../FeedsContainer.style'

const RecommendedPosts = () => {
const { data, isFetching } = useQuery({
queryKey: CACHE_KEYS.recommended(['posts']),
queryFn: getRecommendedPosts,
queryFn: getRecommendedItemsUsingGET,
})

const showSkeleton = isFetching && !data
Expand Down
Loading
Loading