티스토리 뷰

Q1.

왜 비동기 예외처리에서 await을 하지 않으면 try/catch문에서 에러 처리가 불가능할까요?

function sleep(time) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      reject('더 잘래')
    })
  })  
}

try {
  sleep(0) // 프로그램 다운
} catch (error) {
  console.log(error)
}
function sleep(time) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      reject('더 잘래')
    })
  })  
}

(async () => {
  try {
    await sleep(0)
  } catch (error) {
    console.log(error) // 더 잘래
  }
})()

 async 함수에서 await 키워드를 만나면 함수는 실행을 멈추고 함수 전체가 마이크로 태스크 큐로 이동한다. 이후 비동기 처리가 끝나고 다시 콜스택에서 함수가 실행이 된다. 이 때, 함수 내부에는 try/catch가 존재하므로 예외처리가 가능하다.

 

 반면, async/await을 쓰지 않으면 sleep(0)의 프로미스만 마이크로 태스크 큐로 이동한다. 이후 try/catch문이 실행된 뒤 실행컨텍스트에서 사라진다. 그 결과 프로미스의 비동기 처리가 끝난 뒤 콜 스택으로 돌아와 실행될 때 예외처리를 해줄 try/catch문이 없는 상태가 된다. 어느 곳에서도 예외처리가 되지 않기 때문에 프로그램이 다운 된다.

 

 즉, 비동기 함수나 setTimeout 등에서 사용되는 콜백 함수는 호출자가 없다는 것을 주의해야한다. 태스크 큐 또는 마이크로 태스크 큐에 있던 콜백 함수는 콜 스택이 비면 콜 스택으로 푸시되어 실행된다. 이때 콜 스택에 푸시된 콜백 함수의 실행 컨택스트는 콜 스택의 가장 하부에 존재하게 된다. 따라서 에러를 전파할 호출자가 존재하지 않는다.

Q2.

유조는 보통 비동기 함수의 예외처리를 선언형과 명령형 중 어떤 것을 더 선호하시나요? (ex. 비즈니스 로직을 분리하여 사용자는 구체적인 응답상태를 몰라도 사용할 수 있도록 선언적으로 하는 것을 선호한다 등)

 

A. 보통 비동기 함수를 위한 별도의 레이어를 만들어 에러를 처리하는 편이다. 비동기의 예외처리가 서비스 코드 안에 들어오게 되면 불필요한 정보가 많아질 뿐만 아니라 중복되는 코드가 많아지게 된다. 따라서 비동기 예외처리를 위한 레이어를 하나 더 분리하여 해당 레이어 안에서 예외처리를 하고 그 결과를 리턴하는 식으로 코드를 작성하는 것을 선호한다.

Q3.

장바구니 미션을 같이 살펴보며 리덕스에서 어떻게 예외처리를 했는지 같이 이야기 나눠봐요 :)

export const addCartItem = (product: Product, quantity: string = '1') => async (
  dispatch: Dispatch<CartAction>,
  getState: () => RootState
) => {
  try {
    const { cart: prevCart } = getState().cart;

    if (prevCart.find(item => item.productId === product.productId)) {
      throw new Error('상품이 이미 장바구니에 담겨있습니다.');
    }

    const response = await axios.post(`${URL.CART}`, { product_id: product.productId });

    if (response.status !== STATUS_CODE.POST_SUCCESS) {
      throw new Error('장바구니에 상품을 담는데 실패하였습니다.');
    }

    const cartId = response.headers.location.split('/').slice(-1)[0];
    dispatch({
      type: REQUEST_SUCCESS,
      payload: [...prevCart, { ...product, cartId, quantity, isSelected: true }],
    });
  } catch (error) {
    dispatch({ type: REQUEST_FAILURE, error });
    throw error; // addCartItem을 호출한 쪽에서 에러를 핸들링하기 위함
  }
};

 

 

유조 질문 정리

Q1.

try - catch 문을 사용할 때 finally도 함께 사용하시나요? 사용하신다면 사용하신 사례도 알려주심 좋을거 같아요!

A. fetch가 정상적으로 실행되거나 에러가 발생하더라도 요청은 종료가 된 것 이기 때문에 로딩 상태를 false로 만들기 위해 사용했다.

const useOrders = () => {
  const [orders, setOrders] = useState<Orders>([]);
  const { loading, setLoading, responseOK, setResponseOK } = useFetchingStatus();

  useEffect(() => {
    const fetchData = async () => {
      setLoading(true);
      try {
        const response = await axios.get(URL.ORDERS);
        if (response.status !== STATUS_CODE.GET_SUCCESS) {
          throw new Error('주문 목록 조회 실패');
        }
        setOrders(FORMAT_DATA.ORDERS(response.data));
        setResponseOK(true);
      } catch (error) {
        setResponseOK(false);
      } finally {
        setLoading(false);
      }
    };
    fetchData();
  }, [setLoading, setResponseOK]);

  return { orders, loading, responseOK };
};

export default useOrders;

Q2.

Error를 처리할 때 사용자에게 알려줘야 할 Error와 알려주지 않아도 될 Error는 어떤게 있을까요?

에러가 정상적으로 처리되지 않거나 에러가 왜 발생했는지 사용자에게 알려주지 않는다면 사용자는 서비스를 이탈하게 된다. 때문에 여러 Error Case를 분석하고 관리하는 것은 매우 중요하다고 생각한다.

사용자가 직접 해결 가능한 에러이든 아니든 에러가 발생하는 상황 자체로 사용자는 피로감을 느끼게 된다. 따라서 대부분의 에러는 사용자에게 알려주는 것이 좋다고 생각한다. 사용자가 에러를 인지하고 행위를 개선 혹은 중단을 할 수 있도록 알려줘야 한다. 특히 에러의 영향 범위를 작게 가져가 사용자가 구체적으로 상황을 인지하고 다음 선택지가 무엇인지 인지하도록 할 필요가 있다.

그렇다고 해서 모든 에러를 알려주는 것은 바람직하지 않다. 특히 보안 관련 에러를 사용자에게 알려주는 것은 어리석은 행위라고 생각한다. 프로그램의 약점이 무엇인지 해커에게 알려주는 꼴이다.

 

Q3.

에러 경계(Error Boundaries) 를 사용해보신 적이 있으신가요? 사용해보신 적이 있다면 경험을 공유해 주심 좋을거 같아요!

Use react-error-boundary to handle errors in React

기타 인사이트

'try..catch'와 에러 핸들링

효율적인 프런트엔트 에러 핸들링 - JBee

'스터디 > 하브루타 스터디' 카테고리의 다른 글

9. 프로토타입  (1) 2021.06.06
7. Scope and Closure  (0) 2021.05.09
6. This  (3) 2021.05.02
5. 변수와 데이터  (0) 2021.04.25
4. 함수  (0) 2021.04.18