기존에 홈에서는 로그인, 회원가입, 로그아웃 버튼이 나오고 회원가입 페이지에서는 홈 버튼과 로그인버튼, 로그인 페이지에서는 홈 버튼과 회원가입 버튼, 투두페이지에서는 홈 버튼과 로그아웃 버튼이 나올 수 있도록 buttons 배열에 해당 정보들을 넣어놓고 map을 통해서 원하는 버튼을 렌더링 할 수 있도록 구현했었다.
export default function AppHeader() {
const [currentPath, setCurrentPath] = useState('')
const navigate = useNavigate()
const handleLogout = () => {
...
}
useEffect(() => {
setCurrentPath(window.location.pathname)
}, [currentPath])
const buttons = [
{
path: '/',
text: <AiFillHome />,
showOnPaths: ['/signin', '/signup', '/todo']
},
{ path: '/signup', text: '회원가입', showOnPaths: ['/', '/signin'] },
{ path: '/signin', text: '로그인', showOnPaths: ['/', '/signup'] },
{ path: '', text: '로그아웃', showOnPaths: ['/', '/todo'] }
]
return (
<Nav>
<h1>Wanted-Pre-Onboarding</h1>
<FlexDiv>
{buttons.map(
(button) =>
button.showOnPaths.includes(currentPath) && (
<Button
key={button.path}
onClick={() => {
if (button.text === '로그아웃') {
handleLogout()
} else {
navigate(button.path)
}
}}
>
{button.text}
</Button>
)
)}
</FlexDiv>
</Nav>
)
}
하지만 Outlet 다시 공부하면서 AppHeader 컴포넌트를 App에서 공통적으로 정의 하고 난 뒤 부터 원하는 페이지에서 내가 원하는 대로 버튼을 렌더링 시켜주지 못하는 현상이 발생했다.
버튼이 렌더링은 되지만, 원하는 대로 안되고 있다. currentPath를 콘솔로그에 찍어보니, 비어있는 스트링 값을 출력한다. setCurrentPath가 제대로 되지 않고 있는 것이다. 그렇기 때문에 버튼이 원하는대로 렌더링이 안되는 것 같다.
그럼 갑자기 왜 그렇게 된걸까? AppHeader를 App에서 공통적으로 정의하게 되면서 기존에 홈, 회원가입, 로그인, 투두페이지에서 AppHeader를 가지고 있었던 것과는 다른 상황이 되어버렸다. 그러니까 AppHeader 컴포넌트가 렌더링된다고 해서 다른 컴포넌트에 전혀 영향이 없게 된 것 같다.
그래서 일단은 currentPath를 상태로 관리하지 않고 button.showOnPaths.includes(currentPath) 부분을
button.showOnPaths.includes(window.location.pathname)을 해서 바로 반영이 될 수 있도록 해서 해결했다. 하지만 버튼은 제대로 렌더링 되지만, set함수를 호출하지 않으니, 컴포넌트가 리렌더링이 일어나지 않고 그렇게 되니까 App에서 정의해놓은 리다이렉션 코드도 호출되지 않는 상황이다.
그래서 실제로 로그인, 회원가입 페이지 자체에서 리다이렉션 코드를 넣어놓으면 정상적으로 된다. 하지만 App에만 리다이렉션 코드를 정의해놓으면 App컴포넌트 자체를 다시 재호출하지 않기 때문에 useEffect 안에 들어있는리다이렉션 로직이 수행되지 않는다.
이 경우에 그냥 리다이렉션을 하고 싶은 컴포넌트에서 로직을 가지고 있게 해서 해결하면 되는 걸까? 뭔가 문제를 회피한다는 느낌이다.
정리를 해보자면,
일단 오늘은 여기까지 고민해봤다. 이 정도까지만 하고 일단 멘토님한테 한 번 피드백 받아봐야겠다. (주먹구구식의 해결코드는 일단 깃허브에 push해놓았다.)
기존에는 ApiClient 클래스에서 request 메서드에서 공통적으로 에러를 catch해서 처리하고 있었다. 하지만 멘토링날 에러를 처리하는 부분에 대해서 클래스에서 최대한 자세하게 에러에 대한 처리를 api별로 하고 그 에러를 받아서 사용자에게 보여지게하는 로직을 외부 유틸함수를 정의해서 에러를 핸들링하고 표현하는 게 일반적이라는 피드백을 받았다. request 내부의 로직은 아래와 같은데, 일반 여기서 에러를 캐치해서 기존에 error.ts 에서 로직을 처리하고 있었으니, 여기서부터 차근차근 console에 에러 값을 출력해보고 하나씩 api별로 정의를 해보려고 시도했다.
private request<T>(
method: Method,
url: string,
data: Record<string, string | boolean> = {},
config?: AxiosRequestConfig
): Promise<T> {
return this.api
.request({
method,
url,
data: method === 'post' || method === 'put' ? data : undefined,
headers: {
...this.headers,
Authorization: `Bearer ${localToken.get()}`,
'Content-Type': 'application/json'
},
...config
})
.then((res) => res.data)
.catch((error) => {
console.log(`error: ${error}`)
if (error.response) {
console.log(`error.reponse: ${error.response?.data.message}`)
throw error
} else {
throw new AxiosError(error)
}
})
}
일단 회원가입, 로그인 페이지부터 시도했다.
ApiClient클래스에서 request 메서드로 요청을 보낼 때, error가 발생하면 보여줄 에러 메세지를 catch 구문에다 정의했기 때문에, 일단 회원가입과 로그인 페이지의 에러를 잡아서 관리했던 try catch 구문을 삭제했다. 그리고 api에서부터 에러가 제대로 잡혀서 내가 원하는 대로 alert을 통해 보여줄지에 대해서 체크했다.
.catch((error) => {
console.log(`error: ${error}`)
if (error.response) {
console.log(`error.reponse: ${error.response?.data.message}`)
if (error.response.status === 401) {
window.alert('비밀번호가 맞지 않습니다.')
} else {
window.alert(error.response?.data.message)
}
}
})
첫 번째 궁금증..?
일단 첫 번째 궁금증대로 try catch 구문을 삭제했기 때문에 에러가 발생하는 상황들을 하나씩 테스트했다.
로그인, 회원가입 페이지에서는 현재 이렇게 3가지 상황이 있었다. 하지만 로그인, 회원가입 페이지에서 try catch구문을 삭제하고 나니까, 에러를 alert으로 보여주고 나서 해당 페이지에 머물러 있어야 하는데, 에러를 보여준 다음에 바로 api를 다시 호출하더니 회원가입이 완료되었다는 alert을 띄우고 회원가입 페이지에서 로그인 페이지로 리다이렉션 되었다.
로그인 페이지에서는 const res = await signInUser({ email, password }) 의 res 응답값이 없을 수도 있다는 오류가 발생했다.
그래서 Auth에서 다시 try catch 구문을 복구하고 ApiClient에서는 throw해서 에러만 던져 준 다음에 각 컴포넌트에서 에러를 잡아서 처리하는 방식으로 변경했다.
const Auth = ({ title, buttonText }: AuthProps) => {
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const navigate = useNavigate()
const handleEmailChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
setEmail(e.target.value)
}
const handlePasswordChange = (
e: React.ChangeEvent<HTMLInputElement>
): void => {
setPassword(e.target.value)
}
const handleAuth = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault()
const path = window.location.pathname
try {
if (path === SIGN_UP) {
await signUpUser({ email, password })
window.alert(COMPLETED_SIGN_UP)
}
if (path === SIGN_IN) {
const res = await signInUser({ email, password })
if (!res) {
return
}
const { access_token } = res
const saveToken = (token: string) => {
localToken.save(token)
}
if (access_token) {
saveToken(access_token)
}
window.alert(COMPLETED_SIGN_IN)
}
navigate(path === SIGN_UP ? SIGN_IN : TODO)
} catch (error) {
if (error instanceof AxiosError) {
if (error.response?.status === 401) {
window.alert(PASSWORD_ERROR)
} else {
window.alert(error?.response?.data.message)
}
} else {
window.alert(UNKNOWN_ERROR)
}
}
}
멘토님 피드백은 최대한 클래스에서 좀 더 자세하게 처리하고 그걸 받아서 처리하는 곳에서는 간단하게 받아서 처리하라는 내용이었는데, 그렇게 처리하니까 각 컴포넌트에서 에러를 다시 잡아서 처리해야하는 상황이 발생했다.
(아마 내가 잘 모르는 부분이 있으리라… 매번 그랬으니;;)
에러메세지를 클래스에서 처리해서 보여주는 건 가능했으나, 그 이후에 api를 호출하는 게 비동기적으로 이루어지다 보니까 컴포넌트 자체에서 try catch를 하지 않으면 에러를 보여준다음 동작이 원하는 대로 이루어지지 않았다. 회원가입의 경우 에러를 보여주고 갑자기 로그인이 성공해버리는 것과 같은 현상 말이다.
일단은 그래서 어쩔 수 없이 컴포넌트 자체에서 try catch를 해서 에러를 잡는 방법으로 변경했다.
(일단 오늘은 여기까지 고민.. 내일 다시 더 고민해보자.)