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

[오다은] Week19 #492

Merged

Conversation

O-daeun
Copy link
Collaborator

@O-daeun O-daeun commented Jun 30, 2024

요구사항

기본

  • 변경된 api를 활용해 주세요.
  • 모달에 필요한 api 요청을 만들어 기능을 완성해 주세요.
  • api 요청에 TanStack React Query를 활용해 주세요.

주요 변경사항

  • 배포링크 : https://linkbrary-oh.vercel.app/

  • api 주소가 수정되어서 약간의 response 수정이 있습니다.

  • 모달 컴포넌트 분리

  • 모달 컴포넌트 열고 닫는 기능 Context API로 구현

  • 로그인/회원가입 제외 React Query 적용

멘토에게

  • 모달을 하나의 컴포넌트에서 관리하다가 기능별로 나누었습니다.
  • React Query를 잘 작성하였는지 피드백 부탁드립니다!
  • 감사합니다!!

@O-daeun O-daeun added the 매운맛🔥 뒤는 없습니다. 그냥 필터 없이 말해주세요. 책임은 제가 집니다. label Jun 30, 2024
@O-daeun O-daeun requested a review from kiJu2 June 30, 2024 09:44
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 주소 변경으로 인해 기존 GET 함수를 조금씩 수정하였고,
POST, PUT, DELETE 요청을 추가 구현하였습니다.

Copy link
Collaborator

Choose a reason for hiding this comment

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

넵 ! 설명 감사드립니다. 한 번 꼼꼼히 봐볼게염

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 Author

Choose a reason for hiding this comment

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

folder/[[...currentFolderId]]
경로의 currentFolderId를 곳곳에서 사용하고 있어서
Context API로 빼서 관리하였습니다.

Comment on lines +49 to +55
const handleDeleteButtonClick = (e: MouseEvent) => {
e.preventDefault();
setIsVisibleDeleteCardModal(true);
setModal({ isOpen: true, content: `DeleteLinkModal ${id}` });
};
const handleAddFolderButtonClick = (e: React.MouseEvent) => {
const handleAddLinkButtonClick = (e: MouseEvent) => {
e.preventDefault();
setIsVisibleAddInFolderModal(true);
setModal({ isOpen: true, content: `AddLinkModal ${id}` });
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

카드 내부에서 모달을 띄우니 보여지는 모든 카드의 모달이 중복으로 띄워지는 오류가 있어서 우선 구분할 수 있게 ${id}라는 조건을 추가해 두었습니다.
임시방편으로 해결하여서 추후에 포탈이나 다른 방식으로 모달을 띄우고 싶네욥!
기주님은 모달 띄우실 때 어떤 방법을 사용하시는 편인가요???

Copy link
Collaborator

Choose a reason for hiding this comment

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

아하 조건을 추가해서 문제를 해결하셨군요 😊
저 또한 다은님께서 해결하신 방법으로 컨텍스트로 만드는 편입니다 !
또한, 라우팅도 함께 필요한 모달의 경우 NextJs 앱라우터의 패러랠과 인터섹션 라우터로 만들고 있습니다 😊

@kiJu2
Copy link
Collaborator

kiJu2 commented Jul 1, 2024

수고 하셨습니다 ! 스프리트 미션 하시느라 정말 수고 많으셨어요.
학습에 도움 되실 수 있게 꼼꼼히 리뷰 하도록 해보겠습니다.

@kiJu2
Copy link
Collaborator

kiJu2 commented Jul 1, 2024

모달을 하나의 컴포넌트에서 관리하다가 기능별로 나누었습니다.

오호 굳굳 ! 한 번 보도록 할게욤 😊

React Query를 잘 작성하였는지 피드백 부탁드립니다!

넵넵 ! 물론입니다 💪

감사합니다!!

🙇‍♂️🙇‍♂️

Copy link
Collaborator

Choose a reason for hiding this comment

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

넵 ! 설명 감사드립니다. 한 번 꼼꼼히 봐볼게염

import axios from 'axios';

const BASIC_URL = 'https://bootcamp-api.codeit.kr/api';
const BASIC_URL = 'https://bootcamp-api.codeit.kr/api/linkbrary/v1';
Copy link
Collaborator

Choose a reason for hiding this comment

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

base URL은 환경 변수에 저장하시는게 좋습니다!

환경 변수(Environment Variable): process.env에 내장되며 앱이 실행될 때 적용할 수 있는 값입니다!

다음과 같이 적용할 수 있습니다:

// .env.development
REACT_APP_BASE_URL="http://localhost:3000"

// .env.production
REACT_APP_BASE_URL="http://myapi.com"

// 사용시
<a href={`${process.env.REACT_APP_BASE_URL}/myroute`}>URL</a>

왜 환경 변수에 저장해야 하나요?

개발(dev), 테스트(test), 실제 사용(prod) 등 다양한 환경에서 앱을 운영하게 되는 경우, 각 환경에 따라 다른 base URL을 사용해야 할 수 있습니다. 만약 코드 내에 하드코딩되어 있다면, 각 환경에 맞춰 앱을 배포할 때마다 코드를 변경해야 하며, 이는 매우 번거로운 작업이 됩니다. 하지만, 환경 변수를 .env.production, .env.development, .env.test와 같이 설정해두었다면, 코드에서는 단지 다음과 같이 적용하기만 하면 됩니다.

const apiUrl = `${process.env.REACT_APP_BASE_URL}/api`;

이러한 방식으로 환경 변수를 사용하면, 배포 환경에 따라 쉽게 URL을 변경할 수 있으며, 코드의 가독성과 유지보수성도 개선됩니다.

실제 코드 응용과 관련해서는 다음 한글 아티클을 참고해보세요! => 보러가기

@@ -1,6 +1,7 @@
import { AddLink, EditFolderName } from '@/interfaces/api';
import axios from 'axios';
Copy link
Collaborator

Choose a reason for hiding this comment

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

axios instance를 만들어서 사용해보는게 어떨까요 ?

instance를 만들어서 export를 하고 사용해보는 것 정도로 시도해보면 좋을 것 같아요. axios-instance 파일을 만들어서 instance를 생성하고 export한 후 사용해보는건 어떨까요?
다음과 같이 만들어볼 수 있어요:

const baseURL = process.env.NEXT_PUBLIC_LINKBRARY_BaseURL;

const instance = axios.create({
  baseURL: baseURL,
  headers: {
    'Content-Type': 'application/json',
  },
});

export default instance

axios instance

`${BASIC_URL}${userIdParam}/folders${queryParam}`
);
const result = response.data.data;
const response = await axios.get(`${BASIC_URL}/folders${queryParam}`, {
Copy link
Collaborator

Choose a reason for hiding this comment

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

반환 타입을 제네릭을 통하여 다루면 어떨까요?

Suggested change
const response = await axios.get(`${BASIC_URL}/folders${queryParam}`, {
const response = await axios.get<GetFoldersResponse>(`${BASIC_URL}/folders${queryParam}`, {

이렇게 하면 getFolders의 반환 타입도 추론될 수 있습니다 😊

Comment on lines +118 to +133
export async function putFolderName({
folderId,
newFolderName,
}: EditFolderName) {
const response = await axios.put(
`${BASIC_URL}/folders/${folderId}`,
{
name: newFolderName,
},
{
headers: {
Authorization: localStorage.accessToken,
},
}
);
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

만약 instance를 만드셨다면 accessToken은 인터셉터로 관리하실 수 있습니다 !

아래는 axios의 메써드인 interceptors를 통하여 미들웨어로 인가를 처리하는 예제예요 ! 한 번 확인해보시고 적용하시는 것도 고려해보세요 😊😊😊

instance.interceptors.request.use(
  (config) => {
    const accessToken = localStorage.getItem('accessToken')?.replace(/"/gi, '');

    if (!accessToken) return config;
    config.headers.Authorization = accessToken;
    return config;
  },
  (error) => {
    return Promise.reject(error);
  }
);

instance.interceptors.response.use(
  (response) => {
    return response;
  },
  (error) => {
    alert(`ERROR: ${error.response.data.message} `);
    return Promise.reject(error);
  }
);

Comment on lines +15 to +16
const modal = useModal();
const setModal = useSetModal();
Copy link
Collaborator

Choose a reason for hiding this comment

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

만약 useSetModaluseModal이 합쳐진다면 다음과 같이 작성되겠네요 !:

Suggested change
const modal = useModal();
const setModal = useSetModal();
const { modal, setModal } = useModal();

Comment on lines +14 to +19
const addFolderMutation = useMutation({
mutationFn: (newFolderName: string) => addFolder(newFolderName),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['folders'] });
},
});
Copy link
Collaborator

Choose a reason for hiding this comment

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

굳굳 ~! 적절한 뮤테이션과 성공 이벤트네요 !:

invalidateQueries까지 처리해주셨군요 😊

Copy link
Collaborator

Choose a reason for hiding this comment

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

다만, queryKey의 경우 한 곳에서 관리할 수도 있습니다:

export const folderKeys = {
  getFolders: ['folder'],
  getFolderById: (id: string) => ['folder', id]
};

value={text}
onChange={handleTextChange}
/>
<S.StyledButton text='추가하기' type='submit' />
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 +22 to +31
const putFolderNameMutation = useMutation({
mutationFn: ({ folderId, newFolderName }: EditFolderName) =>
putFolderName({ folderId, newFolderName }),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['folders'] });
queryClient.invalidateQueries({
queryKey: ['folder', String(currentFolder.id)],
});
},
});
Copy link
Collaborator

Choose a reason for hiding this comment

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

크으 ~! 깔끔한 mutation !! 👍👍

Comment on lines +18 to +45
const SHARES = [
{
name: '카카오톡',
imageSrc: KakaotalkIcon,
onClick: (e: MouseEvent) => {
shareKakao(e, currentFolder.name, currentFolder.id);
},
},
{
name: '페이스북',
imageSrc: facebookIcon,
onClick: () => {},
},
{
name: '링크 복사',
imageSrc: linkIcon,
onClick: () => {
navigator.clipboard
.writeText(`${window.location.hostname}/shared/${currentFolder.id}`)
.then(() => {
alert('링크가 복사되었습니다.');
})
.catch((error) => {
console.error('복사 실패:', error);
});
},
},
];
Copy link
Collaborator

Choose a reason for hiding this comment

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

훌륭한 컴포넌트 매핑입니다 ! 👍👍👍

@kiJu2
Copy link
Collaborator

kiJu2 commented Jul 1, 2024

굳굳 !! 훌륭합니다. 다은님 😊
코드 정말 흥미롭게 리뷰했어요. 벌써 리액트 쿼리를 능숙하게 다루시는 것 같은데요? 👍👍👍

@kiJu2 kiJu2 merged commit a033961 into codeit-bootcamp-frontend:part3-오다은 Jul 1, 2024
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.

2 participants