Skip to content

Conversation

@cksrlcks
Copy link
Collaborator

@cksrlcks cksrlcks commented Jan 10, 2025

요구사항

기본

  • 자유 게시판 페이지 주소는 “/boards” 입니다.
  • 전체 게시글에서 드롭 다운으로 “최신 순” 또는 “좋아요 순”을 선택해서 정렬을 할 수 있습니다.
  • 게시글 목록 조회 api를 사용하여 베스트 게시글, 게시글을 구현합니다.
  • 게시글 title에 검색어가 일부 포함되면 검색이 됩니다.

심화

  • 반응형으로 보여지는 베스트 게시판 개수를 다르게 설정할때 서버에 보내는 pageSize값을 적절하게 설정합니다.
  • next의 prefetch 기능을 사용해봅니다.

주요 변경사항

  • app router를 사용하여 진행했습니다.
  • nextauth 추가 : 로그인후 가져온 토큰을 nextauth를 통해 세션관리했습니다.
    • 이전 미션에는 context와 localstorage로 유저정보를 관리하였는데, nextjs로 옮기면서 nextauth까지 실습해보았습니다.
  • 랜딩페이지, faq, privacy페이지는 static으로 빌드하도록 했습니다.
  • 중고마켓, 자유게시판에서의 데이터 패칭은 서버에서 하도록 했습니다.
    • 중고마켓쪽은 이전 미션코드 그대로 client에서 데이터 패칭할때와, 서버에서 데이터 패칭할때를 번들링된 사이즈를 비교해보았습니다.
    • ssr로 개선결과 first load js가 거의 절반으로 줄어들었습니다. (227kb -> 125kb)
      제목 없음-2024-11-02-1453

스크린샷

스크린샷 2025-01-10 오전 11 56 32

멘토에게

  • 배포url : sprint-mission-chanki-next.vercel.app
  • 셀프 코드 리뷰를 통해 질문 이어가겠습니다.
  • 헤더 오른쪽의 로그인 or 프로필 버튼의 렌더링 조건을 서버세션을 통해 관리하려고 했는데, 랜딩페이지가 static으로 빌드되어 사전 빌드될 당시에 세션이 없어서 로그인 버튼으로 보이는것 때문에, 클라이언트 사이드에서 세션을 체크해서 프로필 버튼이 보이도록 했습니다.
    • 실제 런타임서버에서 세션이 있지만, 사전 빌드 될 당시에는 세션이 없어서 html에 '로그인'버튼으로 랜더링됨
    • 브라우저에 static 랜딩페이지가 전달이됬을때, 세션이 있지만 static 빌드된 html을 보기때문에 로그인 버튼으로 보이게 됨
    • 첫 방문시 로그인버튼이 잠시보였다가 프로필로 바뀌는건 랜딩페이지 ssg를 위해 trade off해야할것 같습니다.
  • (추가) pr이후에 parallel, route group등을 더 공부해서 적용하여, app 폴더내부를 정리했습니다.
└── 📁app
    └── 📁(auth)
        └── 📁_components
        └── layout.tsx
        └── 📁login
            └── page.tsx
        └── 📁signup
            └── page.tsx
    └── 📁(common)
        └── 📁(board)
            └── 📁addBoard
                └── page.tsx
            └── 📁boards
                └── 📁(lists)
                    └── 📁_components
                    └── 📁@all
                        └── loading.tsx
                        └── page.tsx
                    └── 📁@best
                        └── loading.tsx
                        └── page.tsx
                    └── layout.tsx
                    └── page.tsx
                └── 📁[id]
                    └── loading.tsx
                    └── page.tsx
        └── 📁(etc)
            └── 📁faq
                └── page.tsx
            └── 📁privacy
                └── page.tsx
        └── 📁(landing)
            └── 📁_components
            └── page.tsx
        └── 📁(market)
            └── 📁_components
            └── 📁addItem
                └── page.tsx
            └── 📁items
                └── 📁(lists)
                    └── 📁_components
                    └── 📁@all
                        └── loading.tsx
                        └── page.tsx
                    └── 📁@best
                        └── loading.tsx
                        └── page.tsx
                    └── layout.tsx
                    └── page.tsx
                └── 📁[id]
                    └── 📁@comments
                        └── loading.tsx
                        └── page.tsx
                    └── 📁@detail
                        └── loading.tsx
                        └── page.tsx
                    └── layout.tsx
                    └── page.tsx
            └── 📁modifyItem
                └── 📁[id]
                    └── page.tsx
        └── layout.module.css
        └── layout.tsx
    └── 📁api
        └── 📁auth
            └── 📁[...nextauth]
                └── route.ts

hanseulhee and others added 30 commits January 6, 2025 11:28
- Link태그 변경 (react router에서 next link)
- client 컴포넌트에 'use client' directive추가
- react router에서 next navigation으로 교체
- 페이지 라우팅 설정
- nextjs로 옮기면서 생기는 오류 수정 (로컬스토리지)
- mini, border 프롭 추가
- 다른컴포넌트에서 무한랜더링 유발 방지용으로 useCallback 적용
- 방향 추가
- 사이즈 추가
- 날짜를 공용 컴포넌트로 교체
- 베스트 게시물 (csr)
- 전체 게시물 (ssr)
- 리프레시발급에 axios인스턴스 변경 (인터셉터 무한지옥에 빠짐)
- client경우에는 context에서 인터셉터를 설정하나, ssr용으로 인터셉션 추가로 설정
@cksrlcks cksrlcks requested a review from Lanace January 10, 2025 03:18
@cksrlcks cksrlcks added the 순한맛🐑 마음이 많이 여립니다.. label Jan 10, 2025
Comment on lines 1 to 21
import { auth } from "@/auth";
import axios from "axios";

export const axiosInstance = axios.create({
baseURL: process.env.NEXT_PUBLIC_API_URL,
});

if (typeof window === "undefined") {
axiosInstance.interceptors.request.use(
async (config) => {
const session = await auth();
if (session?.accessToken) {
config.headers.Authorization = `Bearer ${session?.accessToken}`;
}
return config;
},
(error) => {
return Promise.reject(error);
}
);
}
Copy link
Collaborator Author

@cksrlcks cksrlcks Jan 10, 2025

Choose a reason for hiding this comment

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

서버 런타임환경에서 데이터를 요청할때
서버에 세션이 있으면 헤더에 토큰을 넣어보내도록했습니다.
(중고마켓 게시물 상세페이지를 요청할때 토큰을 넣어보내면, 좋아요를 눌렀는지 판별해서 보내주는것 때문에 넣게 되었습니다.)

클라이언트 사이드에서 인터셉터는 프로젝트 루트쪽에 컴포넌트를 끼워두웠습니다. useEffect코드안에서 interceptor를 설정했습니다.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

조금더 고민을 해보니까 "use server"를 이용해보면 좋을것 같아서,
앱시작점에서 "use server" 지시어를 달고 있는 인터셉터 함수파일을 실행하는 방향으로 바꾸었습니다.

Comment on lines +53 to +57
if (accessToken !== data.accessToken) {
// next-auth session update
await update({ accessToken });
error.config.headers.Authorization = `Bearer ${accessToken}`;
}
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

클라이언트 사이드에서 토큰 재발급후에, nextauth에게 어떻게 바뀐 토큰을 알려주는지 찾아보다가
update를 알게되어서 사용했습니다.

Copy link
Collaborator

@Lanace Lanace left a comment

Choose a reason for hiding this comment

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

사실 코드가 너무 많아서 다 확인하진 못했어요ㅠ

근데 전반적으로 너무 잘 작성해주셨네요
혹시 직접 처음부터 다 개발하신건가요??

어느부분은 어딘가에서 가져오신건지 궁금하더라구요...ㄷㄷ
너무 잘 짜주셔서...ㅎㅎ;;

README쪽에 질문처럼 남겨주셨던데, 제가 이해를 잘 못해서... 괜찮으시면 있다가 멘토링떄 같이 볼 수 있을까요??

너무 고생 많으셨습니다ㅎㅎ!!

Copy link
Collaborator

Choose a reason for hiding this comment

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

tip

git ignore 라는 사이트가 있는데,
여기서 사용하시는 기술스텍을 입력하면 알아서 만들어줄꺼에요ㅎㅎ

보통 한번만 잘 만들고 따로 신경쓸일 없어서... 한번 참고해보시면 좋을꺼에요~

Comment on lines +5 to +16
images: {
remotePatterns: [
{
protocol: "http",
hostname: "**",
},
{
protocol: "https",
hostname: "**",
},
],
},
Copy link
Collaborator

Choose a reason for hiding this comment

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

모든 요청에 대해서 열어둘수도 있긴 한데, 실제로는 이렇게 다 여는경우는 별로 없긴 해요...!
내가 사용하는 cdn 만 열거나 하거든요 보통은...

지금은 딱히 문제 안될것같아요ㅎㅎ!

@@ -1,5 +1,5 @@
{
"name": "fe-weekly-mission",
"name": "next",
Copy link
Collaborator

Choose a reason for hiding this comment

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

꼼꼼하시네요 이런부분들도 신경쓰시고...ㅎㅎ

Comment on lines +8 to +11
interface AuthContainerProps {
children: ReactNode;
mode?: "login" | "signup";
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

아래와 같이 사용할 수 있어요ㅎㅎ

interface AuthContainerProps extends React.PropsWithChildren {
  mode?: "login" | "signup";
}

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

안그래도 저번 팀 마지막 멘토링때 배웠는데, 이번 미션에 수정해보겠습니다

Copy link
Collaborator

Choose a reason for hiding this comment

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

궁금한게 있는데, _components 에 앞에 _ 는 어떤 의미인가요??

보통 javascript 에서 앞에 _가 있으면 암묵적으로 외부에서 사용하지 않는 변수나 값이라는걸 의미하는데,
여기에 _가 있길래요ㅠ

Copy link
Collaborator

Choose a reason for hiding this comment

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

아 그리구 보통 component 폴더는 src아래 또는 root 쪽에 두는경우가 많아요ㅎㅎ

왜냐하면 app 폴더 안쪽에서는 페이지나 레이아웃같은 예약어로 된 파일들이 있어야해서요...!
(물론 동작하는데 문제는 없음)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

next에서 제공하는 private 폴더 규칙을 썻습니다.
리액트부터 진행했던 소스라 전부다 app폴더 안으로 들고오지는 못했어요.
저는 app폴더밑에 같이 있는게 편하더라구요

Comment on lines +8 to +11
export type PaginationResponse<T> = {
totalCount: number;
list: T[];
};
Copy link
Collaborator

Choose a reason for hiding this comment

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

제네릭타입도 잘 사용하시네요...!

Comment on lines +16 to +21
const query = `page=${page}&pageSize=${pageSize}&orderBy=${orderBy}&keyword=${keyword}`;
const response = await axiosInstance.get<PaginationResponse<Product>>(
`/products?${query}`
);

return response.data;
Copy link
Collaborator

Choose a reason for hiding this comment

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

물론 위에 있는 코드도 잘 동작할텐데 아래 코드가 조금더 읽기엔 좋은것같긴 해요ㅎㅎ!

const response = await axiosInstance.get<PaginationResponse<Product>>(
    `/products`, {
      params: {
        page,
        pageSize,
        orderBy,
        keyword,
      }
    }
  );

Comment on lines +25 to +30
const response = await axiosInstance.post<ImageUploadResponse>(
"/images/upload",
formData
);

return response.data;
Copy link
Collaborator

Choose a reason for hiding this comment

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

밖에서 form 데이터를 만들어서 API 를 호출할 수 있긴 한데,
그러면 다른 사람들이 uploadProductImage 함수를 호출할떄 formData를 어떻게 만들어야하는지 문서까지 보아야할꺼에요ㅠ

그래서 가능하면 File을 받고, formData를 여기서 만들어주는게 좋을것같긴 하네여ㅎㅎ

Copy link
Collaborator

Choose a reason for hiding this comment

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

아... service 레이어를 생각하신거 보니 spring이나 다른 backend 에 mvc 모델을 Nextjs에 적용하시려고 하셨나보군요ㅎㅎ

물론 나쁘지 않은 접근이에요!

조금 다른건 보통 service layer에서는 비즈니스 로직을 담당하는데, 프론트에서는 그러한 로직을 component 나 page 쪽에서 다루는 경우가 있어요.

실제로도 여기서는 API에서 받아온 data를 전달하는 역할을 하죠... 굳이 따지자면 DAO 정도의 역할을 하고있는것같아요.

그래서 전 보통 ArticleApis.ts 정도로 이름을 짓고 apis로 묶어버리긴 해요ㅎㅎ!

(해주신 방식이 틀린건 아닌데, 다른 프론트엔드 개발자분들이 이해를 못할 수도 있긴 해요)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

api호출함수가 그대로 컴포넌트 레벨로 섞여들어가면, 나중에 여러곳 수정하기가 힘들것 같아서
따로 분리해서 작업해왔었습니다. (갑자기 주소가 바뀌거나, 파라미터가 바껴야하는 상황에)

그런데 생각보다 여러군데서 불러서 쓰지 않는것 같아서 이렇게 까지 해야할까 하긴 했습니다.

),
});

export type ProductFormType = z.infer<typeof ProductFormSchema>;
Copy link
Collaborator

Choose a reason for hiding this comment

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

하나하나 zod 스키마를 다 정의하신건가요?ㄷㄷ
엄청 잘 하셨네요...

@Lanace Lanace merged commit 438ee90 into codeit-bootcamp-frontend:Next-김찬기 Jan 14, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

순한맛🐑 마음이 많이 여립니다..

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants