TIL_2023_08_27

이종현·2023년 8월 28일
0

Today_I_Learned

목록 보기
87/145
post-thumbnail

Today 요약

  1. 버그 해결하기
  2. 에러 처리

1. What I did?

1.1 버그 해결하기

원하는 페이지에서 원하는 버튼 렌더링하는 부분 오류

기존에 홈에서는 로그인, 회원가입, 로그아웃 버튼이 나오고 회원가입 페이지에서는 홈 버튼과 로그인버튼, 로그인 페이지에서는 홈 버튼과 회원가입 버튼, 투두페이지에서는 홈 버튼과 로그아웃 버튼이 나올 수 있도록 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 안에 들어있는리다이렉션 로직이 수행되지 않는다.

이 경우에 그냥 리다이렉션을 하고 싶은 컴포넌트에서 로직을 가지고 있게 해서 해결하면 되는 걸까? 뭔가 문제를 회피한다는 느낌이다.

정리를 해보자면,

  1. Outlet을 어떻게 사용하는 것이 가장 맞는 것일까에 대한 고민을 하다가 모든 페이지에서 공통적으로 가지고 있는 UI인, AppHeader 컴포넌트를 App에 정의하고 Outlet을 통해 나머지 Home, SignUp, SingIn, Todo 컴포넌트를 렌더링하고 난 뒤에, 내가 원하는 대로 버튼이 렌더링 되지 않았다.
  2. 확인해보니, currentPath를 set해주는 useState 함수에가 제대로 동작하지 않았다.
  3. window.location.pathname을 직접 map함수에서 사용해서 해결했지만 뭔가 탐탁치 않다.
  4. 버튼은 해결했지만, App 컴포넌트에서 관리했던 리다이렉션 코드가 정상적으로 동작하지 않았다.
  5. 각 컴포넌트에서 리다이렉션 코드를 실행할 때는 동작하는 걸 확인했다. 그렇다는 건, App 컴포넌트가 url이 변경되면서 Home, SignUp, SingIn, Todo컴포넌트를 렌더링할 때 App에 있는 리다이렉션 로직이 수행되지 않았다는 것이다.
  6. 각 컴포넌트에서 리다이렉션 코드를 넣어서 해결할 수는 있지만, 두 가지가 탐탁치 않았다.
    1. window.location.pathname 코드는 useEffect에서 관리해야 하지 않을까? UI를 그리는 return 내부에서는 상관없는 걸까? 그러니 window.location.pathname을 currenPath에 저장하고 그걸 상태관리가 가능하게끔 해서 return문 내부에서 사용해야 하지 않을까?
    2. 토큰이 있는지 없는지의 여부를 체크하는 건 가장 상위 컴포넌트에서 수행해서 확인하고 리다이렉션 처리를 할 수 있도록 App 컴포넌트 안에 useEffect 코드에서 처리하려고 했다. 하지만 지금 당장의 문제를 해결하기 위해서 각 컴포넌트에서 해결해버리면 그런 의도를 억지로 변경해버리게 되는 것 같다는 생각이 든다.

일단 오늘은 여기까지 고민해봤다. 이 정도까지만 하고 일단 멘토님한테 한 번 피드백 받아봐야겠다. (주먹구구식의 해결코드는 일단 깃허브에 push해놓았다.)

1.2 에러 처리

기존에는 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)
    }
  }
})

첫 번째 궁금증..?

  1. api에서 사용하는 catch 구문은 이미 에러가 발생했을 때 그 에러를 잡아서 catch구문 안에서 에러와 관련된 로직을 작성하면 더 이상 다른 곳에서 try catch 구문을 사용할 필요가 없다?

일단 첫 번째 궁금증대로 try catch 구문을 삭제했기 때문에 에러가 발생하는 상황들을 하나씩 테스트했다.

  • 가입이 되어 있지 않은 이메일로 로그인할 때 - api에 정의되어 있었다. (해당 사용자가 존재하지 않습니다.)
  • 이메일은 일치하지만 비밀번호가 맞지 않을 때 - api에 Unauthorized 로 정의
  • 가입이 되어 있는 이메일로 다시 가입하려고 할 때 - api에 정의되어 있었다.(동일한 이메일이 이미 존재합니다.)

로그인, 회원가입 페이지에서는 현재 이렇게 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를 해서 에러를 잡는 방법으로 변경했다.

(일단 오늘은 여기까지 고민.. 내일 다시 더 고민해보자.)


profile
데이터리터러시를 중요하게 생각하는 프론트엔드 개발자

0개의 댓글