본문 바로가기
source-code/React

react-query와 error 전파 handling

by mattew4483 2023. 8. 16.
728x90
반응형

작년 초 쯤이었을까?

api 요청 및 응답 상태 관리를 redux-thunk에서 react-query로 변경해나가기 시작했다.

 

const getBookApi = async () => {
  const response = await axios.get(URL)
  return response.data
};

const {data} = useQuery('book', getBookApi)

의식의 흐름대로 다음과 같이 썼었는데...

디버깅을 하다보니, 응답에 실패했는데도 아무 곳에서도 에러를 감지할 수가 없었다.

분명 useQuery의 onError 옵션을 통해, 에러 발생을 catch할 수 있다고 되어있는데도!

 

하지만 곰곰히 생각해보면, 당연한 일이다.

왜? useQuery는 두번째 인자로 넘겨준 queryFn이 반환하는 Promise가 rejected 상태일 경우, onError를 실행하는데...

현재 getBookApi 함수는 axios 요청시 HTTP error가 발생하더라도 

→ response.data, 즉 undefined가 담긴 fulfilled된 Promise를 return할 뿐이니까!

 

그럼 어쩌지? → 아하! getBookApi도 axios 요청이 실패할 경우, error를 반환하면 되지 않을까?

const getBookApi = async () => {
  try {
  	const response = await axios.get(URL)
  	return response.data
  } catch(e) {
  	return e.response
  }
};

const {data} = useQuery('book', getBookApi)

당연히 안된다! 

이는 error 발생 시 getBookApi의 반환값이 Promise<e.response>로 바뀐 것 뿐이니까!

(해당 Promise는 여전히 fulfilled일테다)

 

그럼 또 어쩌지? 

const getBookApi = async () => {
  try {
  	const response = await axios.get(URL)
  	return response.data
  } catch(e) {
  	throw e.response // throw를 통해 해당 Promise를 reject
  }
};

const {data} = useQuery('book', getBookApi, {
	onError : (e) => alert(e)
})

초 간단! 

HTTP error 발생 시, catch문이 실행되고,

해당 error를 throw해줌으로서, getBookApi는 rejected상태의 Promise를 반환할 것이다.

→ queryFn이 실패했음이 전파되면서, useQuery의 onError 옵션을 통해 해당 에러를 핸들링해줄 수 있다.

 

그런데 이게 정말 좋은 코드인가?

getBookApi 함수는 어떤 역할을 수행해야할까?

→ axios를 통해 HTTP 요청을 보내고, 해당 응답을 반환하는 것.

즉... 해당 요청의 성공/실패 여부는 getBookApi 함수의 관심사라 볼 수 없다..!

 

그렇다면? 함수 내부에서 try-catch문을 사용해 불필요한 에러 전파를 유도할 필요가 전혀 없지 않은가?

const getBookApi = async () => {
  return await axios.get(URL)
};

const {data} = useQuery('book', getBookApi, {
	onError : (e) => alert(e)
})

이런 식으로!

getBookApi는 axios에서 반환되는 AxiosResponse나 AxiosError 등을 그대로 return해주면 될테다.

 

axios는 error 발생 시 rejected 상태의 Promise를 반환하므로, useQuery에서도 의도처럼 queryFn의 요청 실패를 감지할 수 있는 것!

 

하지만 위와 같이 작성할 경우, (또) 문제가 있다.

const getBookApi = async () => {
  return await axios.get(URL)
};

const {data} = useQuery('book', getBookApi, {
	onError : (e) => alert(e)
})

console.log(data) // AxioseResponse
console.log(data.data) // {id : 0, ...}

useQuery에서는 queryFn의 반환값을 data라는 값으로 사용할 수 있는데...

해당 요청이 성공했을 경우, data에는? AxiosResponse가 담겨있을테다!

(axios.get의 응답을 그대로 return했으므로)

export interface AxiosResponse<T = any, D = any> {
  data: T;
  status: number;
  statusText: string;
  headers: RawAxiosResponseHeaders | AxiosResponseHeaders;
  config: AxiosRequestConfig<D>;
  request?: any;
}

server에서 반환한 데이터는 AxiosResponse.data에 담겨있기 때문에

위와 같이 작성할 경우 data.data와 같은 형태로 해당 값을 조회해야한다!

그리고 매번 .data로 접근하는건? 굉장히 유쾌하지 못하다 :(

(실수 여지↑, 의도 파악 어려움 등...)

 

const getBookApi = async () => {
  return await axios.get(URL)
};

const {data} = useQuery(
    'book',
    async () => {
      const { data } = await getBookApi()
      return data
    },
    {
    	onError : (e) => alart(e)
    }
);

console.log(data) // {id : 0 ...}

react-query + axios 조합시 최고의 사용 형태가 아닐까 싶다..!

 

queryFn 내부에서 직접 axiose response data를 비구조할당한 후 return함으로써,

useQuery의 data에 server에서 반환 받은 응답을 바로 담을 수 있게 된다!


애플리케이션을 개발하다보면 에러를 어디서, 어떻게 다뤄야할지에 대해 많은 고민을 하게 된다.

특히 서버와의 요청-응답이 굉장히 잦은 현 서비스의 특성상...

나도 모르게 별 생각 없이 try-catch문을 모든 비동기 요청에 사용할 때가 있다.

(error log를 찍어야 디버깅이 가능하니)

 

하지만 이는 

1. 해당 에러를 반환하는 주체 파악이 어려움 → 디버깅 ↓

2. 반환된 error의 type 파악이 어려움 → (역시) 디버깅 

3. 불필요한 에러 핸들링 및 전파 코드 중복

과 같은 문제가 있다.

 

따라서 어디까지 에러를 전파할 것인가, 어디에서 해당 애러를 핸들링할 것인가를 항상 염두하며 코드를 작성할 필요가 있겠다!

728x90
반응형