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

feat: axios 세팅 #13

Merged
merged 10 commits into from
Jul 18, 2023
2 changes: 2 additions & 0 deletions .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -59,13 +59,15 @@
"@typescript-eslint/method-signature-style": [ // 특정 메서드 syntax 사용해서 시행
"error"
],
"@typescript-eslint/no-explicit-any": "off",
"unused-imports/no-unused-imports": [ // 사용하지 않는 es6 모듈 import를 찾아 제거
"error"
],
"react/jsx-indent-props": [ // JSX에서 props 들여쓰기 유효성 검사
"error",
"first"
],
"no-restricted-globals": "off",
"react/jsx-curly-newline": "off", // jsx curly 내에서 일관된 줄 바꿈 적용
"react/jsx-one-expression-per-line": "off", // JSX에서 한 줄에 하나의 표현식으로 제한
"react/jsx-props-no-spreading": "off", // JSX props 확산 방지
Expand Down
47 changes: 47 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"@emotion/react": "^11.11.1",
"@emotion/styled": "^11.11.0",
"axios": "^1.4.0",
"query-string": "^8.1.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.14.0",
Expand Down
64 changes: 64 additions & 0 deletions src/api/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import axios, { AxiosError, AxiosRequestConfig, AxiosResponse, InternalAxiosRequestConfig } from 'axios';
import qs from 'query-string';
import { API_TIMEOUT, BASE_URL } from '@lib/const/config';

type APIResponse<T = unknown> = {
success: boolean;
msg: string;
data?: T;
};

export const client = axios.create({
baseURL: BASE_URL,
timeout: API_TIMEOUT,
timeoutErrorMessage: '서버 요청 시간 초과',
paramsSerializer: (params) => qs.stringify(params, { arrayFormat: 'comma' }),
});

const handleHeadersWithAccessToken = (config: AxiosRequestConfig): InternalAxiosRequestConfig => {
const accessToken = localStorage.getItem('ACCESS_TOKEN') || '';
Copy link
Collaborator

Choose a reason for hiding this comment

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

P3:
ACCESS_TOKEN이라는 값도 constant로 관리 되면 좋겠습니다.
다른곳에도 활용이되면서 정적인 값이라면 constant로 분리하는것을 고려해주시면 더욱 프로젝트에 활용하기가 유용할 것 같아요.

Copy link
Owner Author

Choose a reason for hiding this comment

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

const 폴더에서 관리할 수 있도록 분리하겠습니다. 👀

config.headers = {
...config.headers,
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
};

return config as InternalAxiosRequestConfig;
};

client.interceptors.request.use(handleHeadersWithAccessToken);
client.interceptors.response.use(
(response: AxiosResponse) => response,
(error: AxiosError) => Promise.reject(error),
);
Copy link
Collaborator

Choose a reason for hiding this comment

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

interceptor 활용해서 반복되는 request, response에 대한 요청을 처리해주신 부분이 보기 좋네요!

Copy link
Owner Author

Choose a reason for hiding this comment

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

해당 부분은 axios 공식문서를 보고 학습하고 TIL로 남겨두었습니다. 이번 기회에 인터셉터를 직접 사용해 봤네요😌


export const http = {
get: function get<Response = unknown>(url: string, config?: AxiosRequestConfig) {
return client.get<APIResponse<Response>>(url, config).then((res) => res.data);
},
post: function post<Response = unknown, Request = any>(url: string, data?: Request, config?: AxiosRequestConfig) {
return client.post<APIResponse<Response>>(url, data, config).then((res) => res.data);
},
put: function put<Response = unknown, Request = any>(url: string, data?: Request, config?: AxiosRequestConfig) {
return client.put<APIResponse<Response>>(url, data, config).then((res) => res.data);
},
patch: function patch<Response = unknown, Request = any>(url: string, data?: Request, config?: AxiosRequestConfig) {
return client.patch<APIResponse<Response>>(url, data, config).then((res) => res.data);
},
delete: function del<Response = unknown>(url: string, config?: AxiosRequestConfig) {
return client.delete<APIResponse<Response>>(url, config).then((res) => res.data);
},
};

export const handleAxiosError = (error: any) => {
if (axios.isAxiosError(error)) {
const axiosError = error as AxiosError;
console.error('Axios Error:', axiosError);
return axiosError.response?.data as APIResponse;
}
console.log('Unknown Error: ', error);
return {
success: false,
msg: '알 수 없는 오류가 발생했습니다.',
};
};
Copy link
Collaborator

Choose a reason for hiding this comment

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

API 요청에 대한 오류 처리 잘해주신 것 같아요 👍

Copy link
Owner Author

Choose a reason for hiding this comment

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

axios error와 일반 error를 분기해 처리해주었습니다!

2 changes: 2 additions & 0 deletions src/lib/const/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const BASE_URL = 'http://133.186.144.153:3001/api';
export const API_TIMEOUT = 3000;
49 changes: 46 additions & 3 deletions src/pages/SettingPage.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,56 @@
import Header from '@ui/components/layout/Header';
import Body from '@ui/components/layout/Body';
import Menu from '@ui/components/layout/Menu';
import SignButton from '@ui/components/SignButton';
import styleTokenCss from '@ui/styles/styleToken.css';
import styled from '@emotion/styled';
import { useNavigate } from 'react-router';
import { PATH } from '@lib/const/path';

export default function SettingPage() {
const navigate = useNavigate();

const handleClickLogout = () => {
const isLogout = confirm('로그아웃 하시겠습니까?');
if (isLogout) {
localStorage.removeItem('ACCESS_TOKEN');
navigate(PATH.HOME);
}
};

return (
<>
<Header />
<Body>setting page</Body>
<Title>마이 페이지</Title>
<Container>
<SignButton
text="로그아웃"
backgroundColor={styleTokenCss.color.secondaryActive}
color={styleTokenCss.color.white}
onClick={handleClickLogout}
/>
</Container>
<Menu />
</>
);
}

const Title = styled.h2`
background-color: ${styleTokenCss.color.background};
color: ${styleTokenCss.color.gray2};
font-size: 24px;
font-weight: 600;

display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
width: 100%;
height: 100px;
padding: 35px;
`;

const Container = styled(Body)`
padding: 35px;
justify-content: flex-start;
align-items: center;
overflow-y: auto;
`;
32 changes: 24 additions & 8 deletions src/pages/SigninPage.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import Body from '@ui/components/layout/Body';
import styled from '@emotion/styled';
import styleTokenCss from '@ui/styles/styleToken.css';
import React, { ChangeEvent, useMemo, useState } from 'react';
import React, { ChangeEvent, useEffect, useMemo, useState } from 'react';
import { useNavigate } from 'react-router';
import { PATH } from '@lib/const/path';
import { User, UserValidation } from '@lib/types/user';
import NavigationHeader from '@ui/components/layout/NavigationHeader';
import getValidationUser from '@lib/utils/getValidationUser';
import SignButton from '@ui/components/SignButton';
import InputBox from '@ui/components/InputBox';
import { handleAxiosError, http } from '../api/client';

export default function SigninPage() {
const navigate = useNavigate();
Expand Down Expand Up @@ -41,19 +42,34 @@ export default function SigninPage() {
[userValidation.email, userValidation.password],
);

const handleClickSignIn = async () => {
if (!isDisabledSubmit) {
Copy link
Collaborator

@bytrustu bytrustu Jul 18, 2023

Choose a reason for hiding this comment

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

P3:
disabled 일 경우에만 실행되도록 잘 작성해주신 것 같아요.
하지만 코드를 봤을때 한번더 생각되게 만드는것 같습니다.

if (isDisabledSubmit) {
  return;
}

이런식으로 작성해주시면 직관적으로 disabled일 경우, 함수의 동작이 되지 않는다는걸 바로 알 수 있을것 같습니다.

try {
const response = await http.post('/user/signin', {
email: user.email,
password: user.password,
});
Copy link
Collaborator

Choose a reason for hiding this comment

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

P3:
axios를 모듈로 재정의해주시면서 제네릭으로 response, request 타입을 지정되도록 설정을 해주셨는데요.
이부분 활용해서 로그인 함수에도 적용해보시면 좋을것 같습니다.

그러면 각 API요청에 대한 타입을 작성하는것이 필요할 것이고, 이런것들을 하나씩 반영해보시면서 프로젝트를 개선 할 수 있을것 같네요.

그리고 사소하지만
변수명 response보다는 좀 더 명확히 responseSignIn 정도로 변수명을 작성해보시는건 어떨까요?

Copy link
Owner Author

@ShinjungOh ShinjungOh Jul 14, 2023

Choose a reason for hiding this comment

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

post의 응답 타입을 지정했습니다. 이와 관련한 테스트 오류가 있어서 블로그에 [트러블 슈팅] 모듈 시스템과 Jest 테스트
글을 남겨 두었습니다!

const accessToken = response.data.user.user_token;
localStorage.setItem('ACCESS_TOKEN', JSON.stringify(accessToken));
Copy link
Collaborator

Choose a reason for hiding this comment

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

로그인했을때 accessToken만 저장되고 있는것으로 보여지네요.
유저에 대한 정보는 없었을까요? 🤔

Copy link
Owner Author

@ShinjungOh ShinjungOh Jul 13, 2023

Choose a reason for hiding this comment

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

로그인에 성공할 경우 유저 토큰과 name이 함께 넘어옵니다. name도 로컬 스토리지에 저장해 주는 것이 좋을까요? 회원가입 때 입력한 닉네임이 마이 페이지에서 보여지도록 구현하려고 했습니다. 생각해보니 닉네임의 수정 가능 여부도 고려를 해봐야 할 것 같네요.

Copy link
Owner Author

@ShinjungOh ShinjungOh Jul 14, 2023

Choose a reason for hiding this comment

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

스크린샷 2023-07-14 오후 5 45 44

로그인 시 다음과 같이 유저 정보가 넘어옵니다. 우선은 로컬 스토리지에 USER 키값에 오브젝트로 name을 저장하도록 구현했습니다.

  const accessToken = responseSignIn.data.user.user_token;
  const userProfile = {
    name: responseSignIn.data.user.name,
   };
  localStorage.setItem(ACCESS_TOKEN, JSON.stringify(accessToken));
  localStorage.setItem(USER, JSON.stringify(userProfile));

navigate(PATH.CALENDAR);
} catch (e) {
const error = handleAxiosError(e);
alert(error.msg);
}
}
};

const handlePageSignup = (e: React.MouseEvent<HTMLAnchorElement>) => {
e.preventDefault();
navigate(PATH.SIGN_UP);
};

const handleClickSignin = () => {
if (!isDisabledSubmit) {
alert('로그인에 성공했습니다.');
useEffect(() => {
const isAccessToken = localStorage.getItem('ACCESS_TOKEN');
if (isAccessToken) {
navigate(PATH.CALENDAR);
} else {
alert('이메일, 비밀번호를 확인해 주세요.');
}
};
}, [navigate]);

return (
<>
Expand Down Expand Up @@ -82,7 +98,7 @@ export default function SigninPage() {
</InputContainer>
<SignButton
text="로그인"
onClick={handleClickSignin}
onClick={handleClickSignIn}
disabled={isDisabledSubmit}
backgroundColor={styleTokenCss.color.secondaryActive}
color={styleTokenCss.color.white}
Expand Down
29 changes: 24 additions & 5 deletions src/pages/SignupPage.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import Body from '@ui/components/layout/Body';
import styled from '@emotion/styled';
import styleTokenCss from '@ui/styles/styleToken.css';
import { ChangeEvent, useMemo, useState } from 'react';
import { ChangeEvent, useEffect, useMemo, useState } from 'react';
import { useNavigate } from 'react-router';
import { PATH } from '@lib/const/path';
import { User, UserValidation } from '@lib/types/user';
import NavigationHeader from '@ui/components/layout/NavigationHeader';
import getValidationUser from '@lib/utils/getValidationUser';
import SignButton from '@ui/components/SignButton';
import InputBox from '@ui/components/InputBox';
import { handleAxiosError, http } from '../api/client';

export default function SignupPage() {
const navigate = useNavigate();
Expand Down Expand Up @@ -64,9 +65,20 @@ export default function SignupPage() {
[userValidation.email, userValidation.password, userValidation.passwordCheck, userValidation.name, isChecked],
);

const handlePageSignUp = () => {
alert('회원가입에 성공했습니다!');
navigate(PATH.CALENDAR);
const handleClickSignUp = async () => {
try {
const response = await http.post('/user/signup', {
email: user.email,
password: user.password,
name: user.name,
});
const accessToken = response.data.user.user_token;
localStorage.setItem('ACCESS_TOKEN', JSON.stringify(accessToken));
navigate(PATH.CALENDAR);
} catch (e) {
const error = handleAxiosError(e);
alert(error.msg);
}
};

const isError = {
Copy link
Collaborator

Choose a reason for hiding this comment

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

P3:
isError에서 input 항목들의 validation 관리를 해주는것은 좋아보입니다.
조금 더 확장해서 message도 여기서 관리해주면 어떨까요?

const isError = {
  email: {
    error: user.email.length > 0 && !userValidation.email,
    message: '이메일 형식이 올바르지 않습니다.'
  },
  ...
}
<ErrorMessage>{isError.email.error && isError.email.message}</ErrorMessage>

위와 같이 개선해볼수 있겠습니다.

Copy link
Owner Author

Choose a reason for hiding this comment

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

해당 부분 개선했습니다!

Expand All @@ -76,6 +88,13 @@ export default function SignupPage() {
name: user.name.length > 0 && user.name.length < 2,
};

useEffect(() => {
const isAccessToken = localStorage.getItem('ACCESS_TOKEN');
if (isAccessToken) {
navigate(PATH.CALENDAR);
}
}, [navigate]);

return (
<>
<NavigationHeader />
Expand Down Expand Up @@ -139,7 +158,7 @@ export default function SignupPage() {
<SignButton
text="회원가입"
disabled={isDisabledSubmit}
onClick={handlePageSignUp}
onClick={handleClickSignUp}
backgroundColor={styleTokenCss.color.secondaryActive}
color={styleTokenCss.color.white}
/>
Expand Down
Loading