이번 주 멘토링 시간에는 모듈화와 추상화에 대한 내용을 다뤘다.
모듈화와 추상화는 정보처리기사 시험을 준비하면서 들었던 개념이었지만, 활용보단 이론에 더 가까운 단어였다. 하지만 멘토님께서 사례를 통해서 설명을 해주신 덕에 이제는 이 개념을 코드에 녹여내기 위해 어떤 과정을 거쳐야하는지 이해하게 되었다.
그렇다면 모듈화와 추상화가 무엇인지부터 알아보자
모듈화와 추상화
모듈화의 개념적 정의는 아래와 같다.
소프트웨어 설계에서 기능 단위로 분해하고 추상화 되어 재사용 및 공유 가능한 수준으로 만들어진 단위
공부하면서 가장 만나고 싶지 않은, 선행적 지식이 필요한 순간이다. 정의를 알아보는데, 또 다른 단어에 대한 정의를 알아야 이해할 수 있다니..! 추상화는 또 뭔데?!
추상화의 개념적 정의
공통적인 부분을 취하고 차이점을 버리는 일반화 과정
아하 -! 이제 추상화의 정의가 뭔지 알았겠다 다시 모듈화의 개념적 정의를 이해해보자.
기능 단위로 분해하고, 추상화 되어 => 재사용이 가능하도록 공통적인 부분을 추출하여 분해한다
재사용 및 공유 가능한 수준으로 만들어진 단위 => 이렇게 분리한 단위를 필요한 곳에서 가져다 쓸 수 있는 형태로 쪼갠다.
말은 어려웠지만, 우리가 자주 쓰는 기능을 커스텀 훅으로 분리했던 것을 떠올리면 된다.
우리는 본능적으로 모듈화, 추상화를 하고 싶어했던 것이다 -!
이러한 개념적 정의를 넘어, 멘토님께서는 어떤 것들을 고려해야 하는지까지 덧붙여서 설명해주셨다.
모듈화, 추상화를 위해 고려해야할 점
- 어떤 로직을 분리할 수 있을까?
- 변경 사항에 유연하게 대응하려면 어떻게 작성이 되어야 할까?
- 외부 의존성이 필요한가?
- 공통으로 사용될 수 있을까?
나는 이러한 4가지의 고려 사항을 이렇게 이해해보았다.
어떤 로직을 분리할 수 있을까?
=> 왜 이 로직을 분리해야하는지에 대한 고민이 동반되어야 한다
=> 반복되는 로직을 여러 군데에서 사용한다면, 그 로직이 변경되었을 때 여러 파일들을 수정해야한다. 이는 변경사항에 유연하게 대응하지 못하는 것. 고로 동일하지만 반복되는 로직을 대상으로 아래와 같은 고민을 해보면 좋을 것 같다.
변경 사항에 유연하게 대응하려면 어떻게 작성이 되어야 할까?
=> 객체지향의 원칙 중 개방폐쇄의 원칙이 있다. 변경에는 닫혀있고 확장에는 열려있어야한다는 뜻인데, 모듈화 추상화의 개념을 보다보니 이 원칙을 지킬 수 있게 작성이 되면 좋을 것 같다는 생각이 든다.
외부 의존성이 필요한가?
=> 여러 외부 의존성을 가지고 있는 경우, 모듈화를 진행했다고 하더라도 오류의 범위를 가늠하기 어렵기 때문에 꼭 필요한 외부 의존성인지 파악하는 것이 중요해보인다.
공통으로 사용될 수 있을까?
=> 모듈화와 추상화를 하는 목적 중 재사용이 가능해야하는 부분을 만족하기 위해서는 공통으로 사용될 수 있도록 작성하는 것이 중요해보인다.
이렇게 모듈화와 추상화 개념을 알게 되었으니, 이번에는 직접 코드로 그 과정을 거쳐보자.
API 함수 모듈화, 추상화 해보기
1. 어떤 로직을 분리할 수 있을까?
서버로부터 데이터를 가져오는 데이터 패치 함수를 리팩토링 해보려고 한다.
아래는 현재 내가 사용하고 있는 데이터 패치 함수 파일 중 하나이다.
import { axiosInstance } from "./axios/axiosInstance";
// interface 생략
export async function getFolders({
folderId,
}: {
folderId: number;
}): Promise<FolderContentsDataForm[]> {
const query = folderId ? `/folders/${folderId}` : "";
try {
const response = await axiosInstance.get(`/linkbrary/v1${query}/links`);
if(response.data){
return response.data;
}
} catch (error){
throw new Error(error.message);
}
}
그렇다면 현재 코드의 문제점은 무엇일까?
- 다른 파일과 동일한 로직이 반복적으로 사용되고 있다.
- 현재는 axiosIntercepter을 통해서 axiosInstance를 사용하고 있지만, 추후 fetch함수로 변경된다고 가정했을 때 사용하고 있는 모든 데이터 패치함수를 수정해줘야 한다. (하나씩)
- 이는 변경사항에 유연하게 대응하지 못하고 있는 것
- 위와 같은 이유로 확장하기에도 쉽지 않은 코드이다.
2. 변경 사항에 유연하게 대응하려면 어떻게 작성이 되어야 할까?
공통된 로직을 모아둔 함수를 만들어서, 호출하여 사용하는 방법으로 해보려고 한다. 공통된 로직이 한 곳에 모아져 있으니, 이 로직들이 변경된다고 하더라도 이 파일만 수정하면 되기 때문에 기존 코드보다 변경 사항에 유연하게 대응할 수 있을 것이다.
멘토링 때 멘토님의 코드를 봤을 때는 완전히 이해했다고 생각했는데, 혼자서 만들어보려고 하니 여러모로 오래 걸렸다.
3. 외부 의존성이 필요한가?
post 함수의 경우, headers의 content-type이 유동적으로 변경되어야하기 때문에, prop으로 필요한 값을 주입시켜주도록 했다. 만약, headers로 값을 넘겨주지 않을 경우에는 가장 많이 사용되는 타입인 application/json을 기본값으로 넣어주었다.
import axios, { AxiosRequestConfig, AxiosResponse } from "axios";
import { axiosInstance } from "./axios/axiosInstance";
export function createHttpClient() {
async function get<R>(url: string): Promise<R> {
try {
const response: AxiosResponse<R> = await axiosInstance.get(url);
return response.data;
} catch (error) {
if (axios.isAxiosError(error)) {
throw new Error(error.message);
} else {
throw new Error("데이터를 불러오는데 실패했습니다.");
}
}
}
async function del<R>(url: string): Promise<R> {
try {
const response: AxiosResponse<R> = await axiosInstance.delete(url);
return response.data;
} catch (error) {
if (axios.isAxiosError(error)) {
throw new Error(error.message);
} else {
throw new Error("데이터를 삭제하는데 실패했습니다.");
}
}
}
async function put<T, R>(data: T, url: string): Promise<R> {
try {
const response: AxiosResponse<R> = await axiosInstance.put(url, data);
return response.data;
} catch (error) {
if (axios.isAxiosError(error)) {
throw new Error(error.message);
} else {
throw new Error("데이터를 저장하는데 실패했습니다.");
}
}
}
async function post<T, R>(
data: T,
url: string,
headers?: string
): Promise<R> {
try {
const response: AxiosResponse<R> = await axiosInstance.post(url, data, {
headers: {
"Content-Type": headers ? headers : "application/json",
},
});
return response.data;
} catch (error) {
if (axios.isAxiosError(error)) {
throw new Error(error.message);
} else {
throw new Error("데이터를 불러오는데 실패했습니다.");
}
}
}
return {
get,
post,
put,
del,
};
}
그럼 이렇게 공통된 로직을 모아뒀으니, 이제는 적용해볼 시간이다.
기존에 사용하던 함수에 적용해보았다.
export async function getFolders({ folderId }: { folderId: number }) {
const baseHttp = createHttpClient();
const query = folderId ? `/folders/${folderId}` : "";
const response = await baseHttp.get<FolderContentsDataForm[]>(
`/linkbrary/v1${query}/links`
);
return response;
}
4. 공통으로 사용될 수 있을까?
물론이다 -! 공통되던 로직을 추상화하여 모듈화했기 때문에 여러 패치 함수에서 사용할 수 있다. 이를 통해서 나는 중복되는 코드도 줄이고 로직 변경에도 보다 유연한 코드를 작성하게 되었다.
마무리
매번 데이터 패치 함수를 작성할 때마다 반복되는 로직이라는 것을 알면서, 이를 개선하는 방법에 대해 고민해보지 않았던 스스로에 대해 반성했다. 그래도 이번 리팩토링을 통해 다른 로직에 대해서도 의심을 갖고 한 번 더 고민해보는 자세를 기를 수 있을 것 같다.
이번 리팩토링을 하면서 기능을 구현하는 과정보다도 리팩토링을 통해 더 많은 고민과 개선을 해 나가는 것 같다는 생각이 든다. 앞으로도 이렇게 클린 코드에 대한 고민과 개선을 반복해서 성장하고 싶다 -!!!
그럼 오늘은 이만, 안녕~!
'💡뚝딱뚝딱 만들어보자 ~! :) > Linkbrary' 카테고리의 다른 글
[Refactoring] ContextAPI로 Modal 컴포넌트 개선하기 (0) | 2024.04.10 |
---|---|
react-hook-form으로 로그인, 회원가입 기능 구현하기 (1) | 2024.04.06 |
커스텀 훅 도전기 (feat.Intersection Observer) (0) | 2024.04.01 |
40% 부족한 검색 기능 완성 시키기 (0) | 2024.03.24 |