2024-07-01: class를 활용한 API 로직 분리
💭 API 로직에 대한 고민
마이그레이션을 하면서 고민했던 사항 중 하나는 API 로직의 재사용 및 코드량 줄이기였다.
아래는 마이그레이션 전의 useEffect를 통해서 API 로직을 구현했던 코드이다.
useEffect(() => { const getUserTravelLists = async () => { const clearModalState = () => { return setModalState({ state: false, title: "", context: "", buttonText: "", closeFunc: () => {}, }); }; try { const listsArray: TravelListType[] = []; const user = firebaseAuth.currentUser; const getTravelListsByUsers = async (userUid: string) => { const docsState = await getDocs( collection(firestore, `travels`, userUid, "docs") ); docsState.forEach((doc) => { const { travelType, title, departureAt, travelPeriod, destination, id, } = doc.data(); listsArray.push({ travelType, title, departureAt, travelPeriod, destination, id, }); }); return setTraveLists([...listsArray]); }; if (user !== null) { return getTravelListsByUsers(user.uid); } else { return onAuthStateChanged(firebaseAuth, (user) => { if (user) { getTravelListsByUsers(user.uid); } else { return setModalState({ state: true, title: "로그인 세션 만료", context: `장시간 사이트 내에 활동이 없어\n로그인 상태가 해제되었습니다.`, buttonText: "로그인 화면으로 돌아가기", closeFunc: () => { clearModalState(); return navigate("/users/signin"); }, }); } }); } } catch (error) { return setModalState({ state: true, title: "데이터 받기 오류", context: `죄송합니다, 데이터를 받아오지 못했습니다.\n새로고침을 시도하시거나 다시 로그인해주세요.`, buttonText: "알겠습니다.", closeFunc: () => { return clearModalState(); }, }); } }; getUserTravelLists(); }, [navigate]);
위의 코드를 보고 이 글을 읽는 사람은 어떤 생각이 들었는지 모르겠지만, 마이그레이션과 리팩토링을 진행하는 내 입장에서는 다음과 같은 생각이 들었다.
- API 코드 안에서 모든 걸 다 끝내려고 한다. (모달 에러 처리나 예외 사항 처리 등)
- 다른 데에서도 쓸 수 있을 데이터를 불러오는 로직임에도 이 훅을 사용하는 페이지에서만 귀속되도록 만들었다.
이 두 가지의 관점에서 보고 스스로에게 자문했을 때, 이 코드가 좋은 코드라고 생각되진 않았다.
언젠가 저 두 가지 관점을 가지고 리팩토링을 해야겠다고 생각했고, 때마침 이번에 마이그레이션을 진행하다보니, 다른 것들도 고치게 된 거 아예 이 부분도 전부 고쳐야겠다고 마음을 굳혔다.
❗ 그래서, 전회사에서 썼던 방식을 활용해보기로 했다.
월급 체불 중인 그 회사에서 일했을 당시, 웹 개발 코드를 보다가 놀랐던 몇 가지가 있었다. 그 중에서는 API와 관련한 로직을 class에서 관리하고, 이를 로직에 사용하는 방식이었다.
처음 보는 방식이라 신기하면서도, 쓰면 쓸수록 이 부분이 참 마음에 들었는데 그 이유는 다음과 같다.
- 같은 계열의 로직을 하나의 class로 모아서 관리할 수 있음.
- API 로직을 class의 메소드로 불러오므로 재사용성이 높아짐.
- static 메소드를 선언하면 인스턴스를 따로 만들 필요없이 그대로 class를 가져와 사용할 수 있음.
정리하면서 혹시 class가 아닌 object로도 메소드를 만들어서 처리할 수 있지 않을까? 하는 생각이 들어서 object로도 고쳐봤는데 잘 작동되어서 큰 차이를 느끼진 못했다.
아마 코드가 고도화가 된다면 그 둘의 차이점이 명확해질 것이라고 생각하지만, 지금 단계에선 알 수 없다..
👏 정리한 결과, 그리고 만족감.
아무튼 그래서, 위의 장점들을 생각해 마이그레이션과 리팩토링을 진행하면서 이참에 API 코드를 class로 묶어 관리하기로 해봤다.
결과적으로 엄청 만족스러웠다.
class TravelService = { static async getUserTravelList(userUid: string, keyword?: string | null) { try { let travelList: TravelBasicInfoType[] = []; const docsState = await getDocs( collection(firestore, `travels`, userUid, "docs") ); docsState.forEach((doc) => { const { travelType, title, departureAt, travelPeriod, destination, id, } = doc.data(); travelList.push({ travelType, title, departureAt, travelPeriod, destination, id, }); }); if (keyword) { travelList = travelList.filter((data) => { return ( data.title.includes(keyword) || data.destination.includes(keyword) ); }); travelList.sort( (a, b) => new Date(a.departureAt).getTime() - new Date(b.departureAt).getTime() ); } return travelList; } catch (error) { return new Error(convertUnknownTypeErrorToStringMessage(error)); } } }; export default TravelService;
위의 코드가 그 변경된 예시인데, 가장 좋은 점은 이제 어디든 이 class를 가지고 필요한 로직을 가져다 붙이는 것으로 재사용성이 높아졌다는 것이다.
그리고, Auth / Travel / Element 등 각 로직이 어떠한 그룹에 속하는지를 정리할 수 있다는 것도 만족감을 배로 가져다줬다.
위의 메소드가 쓰이는 로직의 예시이다.
useEffect(() => { if (!user?.uid) { return; } const getTravelList = async () => { const listState = await TravelService.getUserTravelList(user.uid); if (listState instanceof Error) { return; } setList(listState); }; getTravelList(); }, [user?.uid, setList]);
위와 같이 이제 API 자체의 코드에 대한 부분은 class로 관리하고, API 전후의 사항들은 hook이나 handler, submit에서 관리하도록 정리할 수 있다.
이런 이유로 API 분리를 class로 묶어서 재사용성을 높여봤다.
혹시 이런 식으로 API 코드를 분리하고 싶은데 어떻게 할지 고민되었던 사람들이 있었다면, 이번 기회에 class나 object를 이용한 방식으로 분리해보는 것은 어떨까.
조금이나마 코드 정리에 대한 해법이 될 수 있었길 바란다.