-
Notifications
You must be signed in to change notification settings - Fork 1
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
feat: axios 세팅 #13
Changes from 6 commits
379ab35
3ba1310
ece1d69
5e382d2
eff4a10
a7b2242
7821c12
8cd43f7
8716487
787845f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
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') || ''; | ||
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), | ||
); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. interceptor 활용해서 반복되는 request, response에 대한 요청을 처리해주신 부분이 보기 좋네요! There was a problem hiding this comment. Choose a reason for hiding this commentThe 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: '알 수 없는 오류가 발생했습니다.', | ||
}; | ||
}; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. API 요청에 대한 오류 처리 잘해주신 것 같아요 👍 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. axios error와 일반 error를 분기해 처리해주었습니다! |
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; |
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; | ||
`; |
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(); | ||
|
@@ -41,19 +42,34 @@ export default function SigninPage() { | |
[userValidation.email, userValidation.password], | ||
); | ||
|
||
const handleClickSignIn = async () => { | ||
if (!isDisabledSubmit) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P3: if (isDisabledSubmit) {
return;
} 이런식으로 작성해주시면 직관적으로 disabled일 경우, 함수의 동작이 되지 않는다는걸 바로 알 수 있을것 같습니다. |
||
try { | ||
const response = await http.post('/user/signin', { | ||
email: user.email, | ||
password: user.password, | ||
}); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P3: 그러면 각 API요청에 대한 타입을 작성하는것이 필요할 것이고, 이런것들을 하나씩 반영해보시면서 프로젝트를 개선 할 수 있을것 같네요. 그리고 사소하지만 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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)); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 로그인했을때 accessToken만 저장되고 있는것으로 보여지네요. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 로그인에 성공할 경우 유저 토큰과 name이 함께 넘어옵니다. name도 로컬 스토리지에 저장해 주는 것이 좋을까요? 회원가입 때 입력한 닉네임이 마이 페이지에서 보여지도록 구현하려고 했습니다. 생각해보니 닉네임의 수정 가능 여부도 고려를 해봐야 할 것 같네요. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 로그인 시 다음과 같이 유저 정보가 넘어옵니다. 우선은 로컬 스토리지에 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 ( | ||
<> | ||
|
@@ -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} | ||
|
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(); | ||
|
@@ -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 = { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P3: const isError = {
email: {
error: user.email.length > 0 && !userValidation.email,
message: '이메일 형식이 올바르지 않습니다.'
},
...
} <ErrorMessage>{isError.email.error && isError.email.message}</ErrorMessage> 위와 같이 개선해볼수 있겠습니다. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 해당 부분 개선했습니다! |
||
|
@@ -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 /> | ||
|
@@ -139,7 +158,7 @@ export default function SignupPage() { | |
<SignButton | ||
text="회원가입" | ||
disabled={isDisabledSubmit} | ||
onClick={handlePageSignUp} | ||
onClick={handleClickSignUp} | ||
backgroundColor={styleTokenCss.color.secondaryActive} | ||
color={styleTokenCss.color.white} | ||
/> | ||
|
There was a problem hiding this comment.
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로 분리하는것을 고려해주시면 더욱 프로젝트에 활용하기가 유용할 것 같아요.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
const 폴더에서 관리할 수 있도록 분리하겠습니다. 👀