횡단관심사, 그게 뭔데요
학습 배경
먼저 ‘횡단관심사’라는 키워드에 관심을 가지게 된 계기는,
여러개의 컴포넌트 혹은 페이지가 중복된 일을 가지고 있는 코드에서 비롯되었다.
지금 레벨4인데..레벨2에 기록해놨던 기록장에 내가 ‘횡단관심사 찾아보기’라고 적어놨던 것을 보았고, 그래서 지금에 와서야 공부해보고자 한다.
그런데 횡단관심사를 공부하면서 깨달은 것은, 결국 리액트 개발을 하면서 자연스럽게 이미 해결한 문제라는 것이다. 다만 그 문제와 해결방안을 명명하지 못했을 뿐이다.
그래도 공부하면서 ‘왜’ 이렇게 해야했는지 다시 한번 되짚어 볼 수 있어서 좋았다.
횡단관심사(Cross-Cutting Concerns) 란
횡단관심사는, 하나의 어플리케이션의 여러 부분에 걸쳐있는 기능을 의미한다. 음 아직 와닿지 않는다.
예를 들어.. 은행 시스템이라고 하면
- 핵심 관심사 (Core Concerns) : 입금, 출금, 이체
- 횡단 관심사 (Cross-Cutting Concerns) : 입금, 출금, 이체 시 동작되는 보안 처리 / 예외 처리
이렇게 나뉘게 된다. 즉 그림으로 보자면 아래와 같다.
횡단관심사는 말그대로 핵심 기능을 가로지르는 기능을 뜻한다.
프론트엔드에서 횡단관심사
그러면 우리가 개발하는 프론트엔드에서 횡단관심사는 대표적으로 뭐가 있을까? 정말 많겠지만 생각나는대로 적어보자면..
로그인이 필요한 서비스에서는 대표적으로
- 로그인 여부 확인
이 있을 것 같다. 모든 (혹은 대부분의) 페이지에 걸쳐서
사용자가 로그인 되었는가?
를 확인해야 하기 때문이다.
혹은 API 통신을 통해 데이터를 가져오는 기능을 가진다면 아래와 같은 것들도 횡단관심사가 아닐까?
- api 요청이 성공되기 전 까지 Loading 상태 처리
- api 요청이 실패 했을 때 Error 상태 처리
이 역시도, 해당 페이지의 핵심 기능과는 분리되었지만 api 통신이 필요한 페이지/컴포넌트의 경우 loading 상태와 error 상태 처리가 모두 필요할테니 횡단관심사라고 할 수 있을 것 같다. 이 외에도 여러 페이지/컴포넌트의 핵심 기능은 아니지만 중복적으로 삽입되어야 하는 기능들을 프론트엔드에서 횡단관심사라고 부를 수 있겠다.
위에서 은행으로 비유했던 그림을 위에서 정의한 대로 프론트엔드 관점에서 다시 그려보자면… 아래와 같겠다.
그래서 이러한 횡단관심사를 어떻게 관리해아할까?
횡단관심사 처리하기
횡단관심사라는 키워드와 반드시 함께 나오는 키워드가 있다. 바로 AOP 이다. AOP 가 뭘까..? 주위의 백엔드 크루들이 AOP 를 공부하느라 힘겨워하는 것을 보았다. AOP 는 Aspect Oriented Programming 의 약자로 ‘관점 지향 프로그래밍’이란 뜻이라고 한다. 관점 지향 프로그래밍은 말 그대로, 어떤 로직을 기준으로 핵심적인 관점과 부가적인 관점으로 나누어서 보고, 그 관점을 기준으로 각각 모듈화 하겠다는 것이라고 한다.
AOP 를 검색하면 이러한 그림이 나온다. 즉 위의 A, B, C 클래스에서 동일한 색의 선은 해당 클래스에 있는 중복되는 메서드/코드이다. 이런 경우 만약 클래스 A 에서 주황색에 해당하는 기능을 수정해야 한다면 클래스 B, C 에서 같은 코드 역시 수정해줘야 한다. 즉 이렇게 흩어져있는 횡단 관심사를 AOP는 Aspect 로 바라보면서 해결한다. 아래 쪽에서 각각의 관심사를 X, Y, Z 로 모듈화 시켰다. 이제 주황색의 기능을 수정하고 싶으면 X 만 수정하면 해당 기능을 사용하는 모든 클래스에 수정된 사항이 적용된다.
AOP 는 즉, 여러군데 흩어져있는 횡단관심사를 하나의 모듈로 분리하여 재사용 할 수 있게 만들어준다. (더 이상 깊게는 들어가지 않겠다!!)
그러면 프론트엔드에서, 정확히는 리액트 프로그래밍에서 AOP 를 어떻게 녹여낼 수 있을까?
리액트에서 횡단 관심사를 처리하는 방법
횡단 관심사를 AOP 스럽게 처리하는 방법은 많겠지만 대표적으로 세가지가 있을 것 같다.
1. 컴포넌트 합성
아래 코드는 내가 프로젝트에서 작성했던 PrivateRouter
컴포넌트이다.
function PrivateRouter() {
const navigate = useNavigate();
const { isLoading, isError } = useQuery(QUERY_KEY.AUTHENTICATION, isAuthenticated);
useEffect(() => {
if (!isError) return;
navigate(PATH_NAME.HOME);
}, [isError]);
if (isLoading || isError) return <Loader />;
return <Outlet />;
}
export default PrivateRouter;
요롷게 PrivateRouter 를 작성하고.. 위의 컴포넌트에 합성해준다면 아래와 같이 로그인 여부 확인이 필요한 페이지에서는 따로 api 요청과 그에 따른 처리를 해줄 필요 없다.
<PrivateRouter>
<Bookmark />
</PrivateRouter>
<PrivateRouter>
<AddChannel />
</PrivateRouter>
2. ErrorBoundary, Suspense 사용하기
- 리액트에서 api 요청에 따른 공통된 에러 처리와 로딩 처리는 어떻게 해줄까? 바로바로 리액트에서 제공해주는 ErrorBoundary와 Suspense 기능을 사용해주면 된다.
<ErrorBoundary>
<Bookmark />
</ErrorBoundary>
- ErrorBoundary 사용법은 리액트 공식문서에 자세하게 설명되어있지만, ErrorBoundary 컴포넌트(클래스형)를 만들어서, 에러 상태일 때 어떠한 컴포넌트를 렌더링 해줄 지 작성해줘야 한다.
<Suspense fallback={<Spinner />}>
<Bookmark />
</Suspense>
-
Suspense는 fallback 에 로딩 상태일 때 어떠한 컴포넌트를 렌더링 해줄 지 넣어주면 된다.
-
아마 각각 data fetching 라이브러리 마다 ErrorBoundary 와 Suspense 옵션을 지정해주는게 있을 것이다. 아무튼 이러한 식으로 리액트에서 제공해주는 ErrorBoundary와 Suspense를 사용하면, 로딩 / 에러 상태 처리를 api 요청이 필요한 매 페이지마다 정의해주기 보다, 한 곳에서 쉽게 관리해줄 수 있어서 유지보수 측면에서 월등히 좋다.
3. 공통되는 로직은 Custom Hook 으로 분리하기
위에서 AOP 를 설명할 때, 공통되는 로직을 모듈화 한다고 했다. 나는 리액트에서 공통된 로직을 모듈화를 해서 매 컴포넌트나 페이지의 생명주기마다 실행해줄 방법은 Custom hook 이라고 생각했다.
위의 두 방법은 ‘상태가 A 일때 특정 화면을 렌더링 해준다’ 와 같이 렌더링 로직을 처리 해줘야 했기 때문에 컴포넌트 합성 방법을 사용해야 했지만, 그게 아니라 특정 비즈니스 로직만 재사용해야 한다면 Custom hook을 정의하는 방법이 가장 깔끔 할 것이다.
이렇게 횡단관심사가 무엇인지, 프론트엔드에서 횡단관심사는 무엇인지, 그리고 리액트에서 횡단관심사를 어떻게 처리해야 할지 아주 짤막하게 알아보았다.
앞서 말했지만, 횡단관심사, 즉 관심사가 흩어져있는 것은 리액트를 어느정도 사용하다보면 자연스럽게 깨닫게 되는 문제점들이기 때문에 리액트를 어느정도 사용해보았다면 자연스럽게 해결하고 있을 가능성이 매우 높다. 그래도 아~ 이게 횡단관심사였구나! 아~ 그래서 내가 이렇게 분리해주었구나~ 와 같은 이유들을 되짚어보는 시간이 되었으면 좋겠어서 글을 썼다.
이렇게 횡단관심사/핵심관심사 라는 개념을 알고, 그에 따라서 로직을 모듈화 하는 방법들을 알고 있다면 프론트엔드 개발 할 때 자신만의 기준이 더 명확해질 것이라고 기대하기 때문이다.