팀 프로젝트 로맨스가 필요해 사이트를 제작하면서 개선점이나 반성할 점에 대해 기록하기 위해 적었습니다.
기본적으로 스크립트가 실행될 때, 에러가 발생한다면 그 즉시 스크립트를 중단하고 콘솔에 에러를 출력시킨다. 여기서 try-catch는 스크립트의 중단을 막고 에러를 핸들링하는 문법이다.
try-catch는동기
적으로 처리가 되기 때문에 API를 호출하는 비동기 처리가 필요한 부분은 async-await 를 사용해야 한다.
또한, 런타임 환경에서만 동작한다. 즉, 문법적으로는 올바른 코드이어야지 작동한다는 것이다. try절에서 에러가 발생할 경우 그 즉시 try 내부의 코드는 작동을 멈추며 catch절로 이동하여 error 객체를 내뱉는다.
여기서 궁금한 점은 생성된 에러 객체를 어떻게 활용하는가에 있어 의문점이 있었다.
만약 예기치 않은 에러(서버에러, 무언가 알수없는 에러 등)가 발생하였을때, 이러한 점을 실제 서비스를 이용하는 유저들에게 어떤 방식으로 전달하는게 옳고, 개발자입장에서 어떤 식으로 error를 디버깅하기 쉽도록 보여줄 수 있는가?
통신에러 이외에도 정말 여러가지 상황의 에러가 발생할 수 있는데 이걸 모두 캐치해서 처리를하는 것은 불가능할 것이다. 그렇다면 어느 범위까지 캐치를 해야되는가?
export const getSearchUser = async (
data: ISearchInputData
): Promise<AllUsers[]> => {
const { keyword, accessToken } = data;
try {
const response = await api.get(
`/admin/user/search?keyword=${keyword.user}`,
{
headers: { Authorization: `Bearer ${accessToken}` },
}
);
return response.data;
} catch (error) {
if (axios.isAxiosError(error)) {
const { response } = error;
throw {
message: response?.data?.message || "Server Error",
status: response?.status || 500,
data: response?.data,
};
} else {
throw error;
}
}
};
프로젝트 내의 관리자 페이지에서 유저들을 검색하는 API 함수이다.
여기서 만약 try 절에서 에러가 발생한 경우, catch절로 이동해서 error객체를 출력하게 될 것이다. 여기서 error의 종류도 여러개이기 때문에 가장 가능성이 큰 통신에러를 세분화하여 처리를 하였다.
하지만 프론트엔드 측에서의 한계점은 분명하다. 이러한 에러에 대한 예외처리는 백엔드에서 error에 대한 message부분을 설정하지 않으면 "Server Error" 가 작동할 것이다. 이는 디버깅이나 유저에게 보여질 에러에 대한 설명이 충분치 않다고 생각한다.
이러한 점에 대해 더 고민하고 에러 핸들링의 개선점을 요청했어야 한다는 생각이 들었다. 하지만 그렇다고 코드안에 alert() 을 넣어서 에러처리를 하고 싶지는 않았다. 모달 컴포넌트를 import 해서 쓰고 싶었지만, 내가 만든 API 호출 함수는 순수하게 요청에 대한 처리만을 하고 싶었지, 코드 중간에 Modal을 호출하거나 하고 싶지 않았다. 결국 이 코드는 에러가 발생했을 시 유저에게 무슨일이 일어났는지 알려줄 수 없기 때문에 개선할 필요가 있는 코드라고 생각한다.
위의 예시 코드에서는 new Error 객체를 사용하지 않고 catch절의 인수로 error 를 출력하게 하였다. 사실 프로젝트를 진행하면서 catch절의 error 인수와 new Error() 객체의 차이점을 잘 알지 못하였다. 어짜피 보여주는건 같다고 생각했기 때문에. 하지만 회고록을 작성하면서 new Error() 객체가 보여주는 것이 더 명확하다는 것을 알게 되었다.
에러 처리를 위해 throw new Error(name, message) 형태로 작성할 수 있다. 에러 상황에 대한 이름, 메세지를 커스텀할 수 있다. 여기서 new Error()를 사용하지 않고, throw {any} 어떠한 형태로든 객체로 전달할 수 있다. 물론 문자열이나 숫자, 뭐든지 가능하다.
ECMA-262, 3rd Edition actually specifies seven error object types. These are used by the JavaScript engine when various error conditions occur and can also be manually created:
여기서의 내용은 실제 7가지로 오류 개체 유형을 지정한다고 나와있다. 대표적인 에러 객체를 알아보자.
Error - base type for all errors. Never actually thrown by the engine.
ReferenceError - thrown when an object is expected but not available, for instance, trying to call a method on a null reference.
SyntaxError - thrown when the code passed into eval() has a syntax error.
eval()는 문자열로 이루어진 JS 코드를 수행하는 함수이다.
https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/eval
TypeError - thrown when a variable is of an unexpected type. For example, new 10 or "prop" in true.
EvalError - thrown when an error occurs during execution of code via eval()
RangeError - thrown when a number is outside the bounds of its range. For example, trying to create an array with -20 items (new Array(-20)). These occur rarely during normal execution.
URIError - thrown when an incorrectly formatted URI string is passed into encodeURI, encodeURIComponent, decodeURI, or decodeURIComponent.
이러한 다양한 유형의 에러가 있다는 것을 이해하면 문제가 발생했을 시, 더 자세하게 문제를 찾고 디버깅하는데 더욱 편리할 것이다.
try {
//....
} catch(error) {
if (error instanceof TypeError) {
// handle the error
} else if (error instanceof ReferenceError) {
// handle the error
} else {
// handle all others
}
}
생성자 new 키워드를 통해 새로운 에러 객체를 생성한 후, 해당 에러를 throw 하게 된다. 이렇게 생성된 객체에는 스택 트레이스, 메세지 등 에러에 관한 추가적인 정보를 포함한다. 곧, 디버깅이나 예외처리에 있어 유용하게 사용될 수 있다는 것이다.
스택 트레이스(stack trace)
코드 실행 중에 호출된 함수들의 호출 순서와 관련된 정보를 포함하는 추적정보이다. 에러가 발생했을 때 스택 트레이스는 발생한 지점에서부터 호출된 함수들의 역순으로 보여준다. 에러가 발생한 시점부터 따라가며, 각 함수의 이름, 파일 위치, 라인 번호 등과 같은 정보를 제공한다. 이를 통해 개발자는 어떤 함수에서 에러가 발생했는지, 그 함수가 어떤 호출 경로를 거쳤는지, 어떤 파일과 라인에서 문제가 발생했는지 등을 확인할 수 있다.
하나의 함수에서 여러개의 await를 사용하게 된다면 어떻게 될까? await는 동기
적으로 처리하게 되므로 하나의 작업이 끝날때 까지 기다리면서 순서대로 코드가 처리될 것이다.
const handleBoardDelete = async () => {
try {
await deleteUserBoard({ accessToken, id: Number(getDeleteId) });
const response = await getAllBoards(accessToken);
setAllBoards([...response]);
} catch (error) {
throw error;
} finally {
setOpenModal(false);
}
};
위의 코드는 프로젝트코드의 일부이며 게시글을 삭제하는 함수이다. delete요청을 기다리고 정상적인 요청이 수행되고 변화된 게시글 리스트들을 호출하기 위해 getAllBoards라는 API요청 함수를 실행하게 된다. 이와 같은 과정은 순차적
으로 수행되어야 하기때문에 문제가 없지만, 만약에 서로 독립된
일을 처리한다면 이와같은 코드의 작성은 기다릴 필요가 없는데 기다리게 되어 처리 속도가 늦어지는 병목현상이 발생할 것이다. 이에 대해 2가지 해결 방법이 있다.
Promise.all() 을 통해 요청을 한데 묶어 동시에 처리하여 해결할 수 있다.
async function main() {
await Promise.all([first(1), second(2), third(3)]);
}
비동기 처리의 first, second, third 함수가 있고, 그리고 그 작업들은 동일하게 1000ms 에 처리가 완료된다 가정하자. Promise.all 내부의 비동기 처리의 함수는 병렬적으로 작동하며 예상되는 처리시간은 1000ms 로 결과가 나오게 된다. 또한 그 결과물의 리턴 즉, .then() 을 사용하면 배열 형태로 저장되어 새로운 Promise 객체를 반환하게 된다. 여기서 배열에 저장되는 순서는 먼저 해결된 순으로 저장된다.
만약 Promise.all의 인수로 받은 배열의 Promise 중 하나라도 rejected 상태가 된다면 나머지 Promise는 fulfilled 상태를 기다리지 않고 즉시 종료하게 된다. 반환 값은 가장 먼저 rejected 된 Promise 에서의 에러가 .catch() 를 통해 전달된다.
async function main() {
const runA = a();
const runB = b();
const res = await runB;
const runC = c(res);
await runA;
await runC;
}
위의 코드에서 a()함수는 2000ms, b()함수는 1500ms, c()함수는 b함수의 리턴값인 1000이라는 값을 받아 실행하며 1000ms 의 처리 시간이 걸린다 가정하자.
이러한 경우 a함수 자체는 b, c함수와 별개의 독립적인 함수이기 때문에 병렬적으로 처리해도 문제가 없다. 그러므로 c함수를 작동시키기 위해 b함수를 먼저 await 동기처리를 하고, 그 리턴값을 c함수의 파라미터로 넣어준다 그 후 a함수와 c함수를 작동시키면 예상되는 처리 시간은 2500ms 일 것이다.
왜냐하면, runB가 1500ms 실행되는 동안 runA도 실행 중, 즉 동기처리를 하는 부분이기 때문에 500ms 를 기다릴 것이다. 똑같이 runC 부분도 500ms 를 기다릴 것이며 runA가 500ms 실행 중에 runC도 실행 중이었기 때문이다.