프론트엔드 개발을 하면서 우리는 다양한 비즈니스 로직을 다루고 있어요.
자바스크립트가 존재하지 않던 예전의 웹은 정말 UI만 다루고 있었다고 얘기할 수 있겠지만, 자바스크립트가 탄생하고 AJAX 프로그래밍을 통해 웹에서 서버와 통신을 하게 되면서 웹의 역할이 늘어나게 되었어요.
이제는 프론트엔드와 백엔드의 영역이 분리되었고 프론트엔드에서의 책임이 많이 증가하게 되었는데요.
그 책임중의 하나가 비즈니스 로직이라고 얘기할 수 있을 것 같아요.
우선 비즈니스 로직의 정의는 어떻게 내리면 좋을까요?
비즈니스 로직이란 현실 세상의 문제를 해결하는, 소프트웨어 기능의 핵심적인 처리를 담당하는 코드라고 얘기할 수 있을 것 같아요.
예를 들면 쇼핑몰에서의 주문하기, 블로그에서의 게시글 작성하기와 같은 기능들은 모두 비즈니스 로직이라고 얘기할 수 있어요.
그런데 한편에서는 도메인 로직이라는 얘기도 많이 하는 것 같아요.
도메인 로직은 무엇일까요?
도메인 로직이란 코드에서 도메인에서의 의사 결정이 담겨있는 부분을 의미해요.
그러면 도메인이란 무엇인지 알면 좋을 것 같아요.
도메인이란 우리가 코드로 해결하려고 하는 문제를 의미해요.
만약 채용 관리 솔루션이 있다고 하면 여기서의 도메인은 채용 관리 업무라고 볼 수 있어요.
그리고 채용 관리 업무를 하는 데 있어서 하나의 공고에 지원자는 몇명까지 지원할 수 있는지, 공고를 몇개까지 작성할 수 있는지와 같은 로직들은 도메인 로직이라고 할 수 있어요.
그렇다면 비즈니스 로직과 도메인 로직은 같다고 얘기할 수 있을까요?
거의 동등하게 표현해도 괜찮다고 생각하지만 도메인 로직이 더 좁은 범위가 된다고 생각해요.
실제로 도메인과 직접적으로 연관이 있는, 정책과 관련되어 있는 로직을 얘기하고 있기 때문이에요.
다시 돌아와서 비즈니스 로직에 대해 본격적으로 이야기를 해보면 좋을 것 같아요.
매일 만나는 비즈니스 로직
우리는 매일 개발을 하면서 비즈니스 로직을 다루고 있어요.
그리고 비즈니스 로직은 컴포넌트와 상호 작용을 하면서 동작하게 되는데요.
예를 들어서 게시글 삭제 버튼을 누른다면 게시글을 삭제하는 비즈니스 로직이 실행되고, 정상적으로 완료되었다면 해당 게시글을 담고 있던 상태값을 변경해주면 게시글이 지워지는 것을 확인할 수 있을 것 같아요.
비즈니스 로직이란 이렇게 서비스를 구성하는 데 있어서 꼭 필요한 로직인데요.
그런데 서비스의 기능들이 점점 많아지고 복잡도가 높아지면 어떻게 될까요?
하나의 컴포넌트가 5~6개의 비즈니스 로직을 처리하기 위해 코드량이 늘어나고 코드 라인수가 1000줄을 넘어가게 된다면 유지보수하기 매우 어려운 폭탄같은 컴포넌트가 되어 버릴 것 같아요.
시간이 지남에 따라 서비스는 새로운 기능들이 추가되고 점점 복잡도가 높아져갈탠데 우리는 여기서 어떤 선택을 할 수 있을까요?
우리는 이 상황에서 비즈니스 로직을 컴포넌트에서 분리하는 시도를 해볼 수 있어요.
여러개의 비즈니스 로직이 한곳에 뭉쳐있어 코드를 이해하기 어렵게 만든다면 분리하는 선택이 가장 합리적일 것 같아요.
리액트의 컴포넌트에서 비즈니스 로직을 분리하는 건 어떻게 할 수 있을까요?
화면에 그려지는 것을 담당하는 것은 컴포넌트가, 로직과 관련된 코드는 리액트의 커스텀훅을 이용하여 관심사를 분리를 해보는 것이에요.
일반적인 도메인 객체만으로 로직을 분리하지 않고 커스텀 훅으로 비즈니스 로직을 분리하는 이유는 리액트는 상태를 기반으로 UI를 그리는 라이브러리이기 때문인데요. useState 혹은 useReducer를 이용하여 상태를 만들고 상태를 변경시키는 방향으로 비즈니스 로직을 동작시켜야 리액트에게 화면에 UI를 그리도록 위임할 수 있어요. (useSyncExternalStore와 forceUpdate 방식으로 React의 상태가 아닌, 외부 값으로도 컴포넌트와 싱크를 맞출 수 있어요!)
이렇게 분리하고 나면 기존의 코드보다 훨씬 명확하게 컴포넌트의 동작을 이해할 수 있어요.
각각의 커스텀훅 이름을 보면 어떠한 비즈니스 로직을 다루고 있는지 이해할 수 있고 해당하는 훅이 어떤 값들을 반환하는지를 확인하면 의도를 더욱 명확하게 확인할 수 있어요. 이제 컴포넌트에서 비즈니스 로직이 분리되었다고 생각할 수 있을 것 같은데요. 여기서 조금 더 해결 해야 할 문제들이 남은 것 같아요. 비즈니스 로직이라는 단어는 큰 범위의 이야기를 하고 있기 때문이에요. 리액트에서의 상태를 변경시키고, 특정 조건에 따라서 유저의 속성을 파악하거나, 이 값을 서버로 보내기 위해 서버에 api를 보낸다거나, 서버로부터 받아온 데이터를 정제하는 등 프론트엔드에서 백엔드까지 협력하는 과정에서 많은 과정이 필요해요. 바로 이 과정들을 또 별개의 객체로 분리해볼 수 있어요. 일반적으로는 아래와 같이 나눠보면 괜찮을 것 같아요.
- 상태를 변경하는 책임을 갖는 객체
- 해당 도메인에서만 사용되는 로직에 대한 책임을 갖는 객체
- 백엔드 서버에게 api 요청을 보내는 책임을 갖는 객체
- http 통신에 대한 책임을 갖는 객체 정도로 나눠보면 괜찮을 것 같아요.
그런데 여기서 객체란 무엇일까요?
객체라고 하면 object를 떠올릴수도 있을 것 같고 객체 지향에서의 객체를 떠올릴 수도 있을 것 같아요.
여기서는 객체 지향에서의 객체에 대해 간략하게 얘기를 해보려고 해요.
객체는 상태와 행위를 갖고 있어요. 프로퍼티와 메소드라고 생각하면 좋을 것 같아요.
이러한 객체들은 캡슐화를 통해서 필요한 정보만 외부에 노출하고 다른 객체들과 메시지를 주고 받으면서 현실 세계의 문제를 해결하고 있어요.
프론트엔드에서도 객체 지향이 적용된다고 볼 수 있는데요.
객체를 꼭 Class와 연관지어서 생각할 필요는 없어요. 단지 Class는 객체를 생성하기 위한 보조 도구에 불과하기 때문이에요. 객체를 하나의 책임을 가지고 다른 객체들과 상호 작용을 하는 것으로 생각해보면 좋을 것 같아요. 그러면 컴포넌트도 객체라고 볼 수 있어요. 컴포넌트는 prop을 통해서 외부와 메시지를 전달받으면서 동작하고 있으니까요.
이제 앞에서 얘기하고 있던 비즈니스 로직에서도 별개의 객체로 분리하는 얘기를 해보면 좋을 것 같아요. 실제로 위에서 얘기했던 프론트엔드와 백엔드가 협력하는 과정속에서 일어나는 일들을 객체로 분리해볼게요.
아래는 장바구니를 다루는 useCart 라는 커스텀훅이에요. 일부 기능만 작성하였는데, 여기서는 cart라는 장바구니를 나타내는 상태값을 가지고 있고 장바구니에 아이템을 추가할 수 있는 addItemsInCart라는 함수가 보여요.
아래 훅은 메인으로 장바구니에 대한 상태를 변경하는 책임을 갖고 있어요. 그리고 이 책임을 갖고 있는 커스텀훅에서 추가적으로 도메인에서만 사용되는 코드, 백엔드 서버에게 api 요청을 보내는 코드가 보이는 것 같아요. 이 코드들도 별개의 객체로 작성해주었어요. 구현부를 한번 살펴볼게요.
function useCart() {
const [cart, setCart] = useState<Cart[]>([]);
const addItemsInCart = useCallback(async (item:Item) => {
if(alreadyInCart(cart, item)){
toast.open('이미 장바구니에 담긴 상품이에요.');
}
try {
await CartService.addItemsInCart(item);
setCart(item);
} catch(error){
if(error instanceof AxiosError) {
toast.open(error.message);
return;
}
toast.open('일시적인 에러가 발생했어요.');
}
}, [])
return {
cart,
addItemsInCart
}
}
아래 코드는 alreadyInCart라는 장바구니에 제품이 이미 존재하는지 확인할 수 있는 도메인에서 사용되는 로직이에요. 아래 코드는 장바구니라는 도메인에서만 사용되는 도메인 로직이기 때문에 별도의 객체로 분리해두었어요.
export function alreadyInCart(cart:Cart, item:Item) {
return cart.find(cartItem => cartItem.id === item.id);
}
아래 코드는 백엔드 서버에게 api 요청을 보내는 로직이에요. httpClient가 무엇인지 아직은 잘 모르겠지만 대략적으로 서버에게 /cart라는 엔드포인트로 item을 보내면 장바구니에 추가가 되는 것 같아요. 그러면 httpClient에 대해서도 한번 살펴보면 좋을 것 같아요.
export class CartService {
static async addItemsInCart(item): Promise<AddItemsInCartResponse> {
const { data } = await httpClient.post('/cart', { item });
return data;
}
}
httpClient는 이런 로직이에요. axios라는 http 요청을 보낼 수 있는 XMLHttpRequest라는 함수를 기반으로 만들어져 있는 외부 라이브러리를 사용하여 서버와 통신할 수 있는 객체를 만들어주었어요. 추가적으로 interceptors를 이용해서 http 통신을 하는 과정속에서 request, repsonse에 대해 전처리를 해줄 수 있어요.
export const httpClient = axios.create(
baseURL: process.env.API_BASE_URL,
timeout: 3_000,
);
httpClient.interceptors.request.use(requestHanlder);
httpClient.interceptors.response.use(responseHanlder);
지금까지 비즈니스 로직에서도 각각의 책임에 따라서 객체로 분리했던 코드들을 살펴볼 수 있었는데요. 앞에서 봤던 useCart라는 훅은 이렇게 각 객체들간의 협력으로 이루어져 있어요. 객체들은 서로 메시지를 주고 받으면서 비즈니스 로직을 완성하고 있어요. 이러한 코드에서 만약에 api의 응답값이 변경이 되어서 화면에 그려질 응답값으로 정제하기 위한 요구사항이 생긴다면 어떻게 될까요? 당장 생각했을 때는 CartService라는 함수에서 api로 부터 받아온 데이터를 반환해주기전에 데이터를 정제하고 반환해주면 충분할 것 같아요. 그런데 이렇게 되면 CartService라는 클래스의 책임이 백엔드 서버에 api를 요청하는 것 외에도 데이터를 정제하는 책임도 생겨서 두 가지의 일을 하고 있는 것 처럼 보여요. 여기서 우리는 어떻게 할 수 있을까요? 바로 이전에 했던 것 처럼 하나의 비즈니스 로직에서도 각 책임별로 여러개의 객체로 분류했듯이 여기서도 데이터를 정제하는 책임을 함수를 별도의 객체로 분리해보면 좋을 것 같아요. 폴더명은 serializers는 어떨까요?
위에서는 리액트 컴포넌트에서 커스텀훅과 객체들을 이용하여 비즈니스 로직을 분리하는 것에 대한 얘기를 해봤는데요. 아예 상태값을 이용하지도 않고, 컴포넌트의 바깥에서 비즈니스 로직을 다루는 방법도 있어요. 그리고 이렇게 비즈니스 로직을 다루기 위해 리액트는 useSyncExternalStore라는 훅을 제공하고 있어요. 커스텀훅에서는 리액트의 상태와 엮여있었는데 이렇게 작성하면 아예 리액트에서 벗어나 비즈니스 로직을 작성할 수 있어요. 그리고 그렇게 된다면 UI를 표현하는 방식이 리액트에서 뷰로 바뀌거나, 앱으로 바뀌거나 해도 동일한 비즈니스 로직을 재활용 할 수 있구요. 그러면 아예 리액트 바깥에 비즈니스 로직을 작성하는 게 좋을까요? 이 부분에 대해서는 추가로 글을 작성해보려고 해요.
정리
- 컴포넌트는 시간이 지남에 따라 비즈니스 로직이 많아지면 복잡해진다.
- 이런 문제를 해결하기 위해 비즈니스 로직을 커스텀 훅으로 분리한다.
- 커스텀 훅 내부에서도 여러개의 책임이 있다, 이 책임도 각각 분리해준다.
- 리액트의 상태와 관련 없이 리액트 바깥에서 비즈니스 로직을 작성할 수도 있다.
'TIL > 개발' 카테고리의 다른 글
[리팩토링 2판 스터디] 1회차 정리 (0) | 2023.11.18 |
---|---|
요즘 개발자 베타리딩 - 2주차 (0) | 2023.11.09 |
React에서 useCallback은 언제 사용해야 할까? (0) | 2023.11.08 |
BFF(Backend For Frontend)는 어떤 문제를 해결하나? (0) | 2023.11.07 |
[Astro] Astro 3.0에서 달라진 것들 (0) | 2023.09.01 |