
배경
2024년 9월에 있었던 일입니다.
당시에도 그렇고 지금도 저는 웹뷰 기반의 하이브리드 앱 개발 환경에서 웹 프론트엔드 개발을 담당하고 있습니다.
어느 날, 회사에서 신 서비스 기획서를 검토하는 과정에서 앱 업데이트를 하지 않으면 해당 서비스 이용 시 이슈가 있을 것으로 판단하여 앱 버전을 업데이트한 유저만 해당 서비스 화면을 노출하도록 구현하였습니다.
설명에 대한 이해를 돕기 위한 예시 코드는 아래와 같습니다.
// ServiceMainPage.tsx
import { useEffect, useState } from 'react';
const SERVICE_VERSION = '3.1.2';
const ServiceMainPage = (): React.ReactElement | null => {
const [isVisiblePage, setIsVisiblePage] = useState<boolean>(false);
const isSupportVersion = (
curVersion: string,
supportVersion: string,
): boolean => {
// 현재 버전이 서비스 지원 버전 이상인지 확인하여 boolean을 반환하는 로직
// ...
};
const getCurVersion = async (): Promise<string> => {
// 네이티브로부터 현재 앱 버전을 전달 받아 해당 버전을 문자열로 반환하는 로직
// ...
};
const handleVisiblePage = async (): Promise<void> => {
const curVersion = await getCurVersion();
const isSupport = isSupportVersion(curVersion, SERVICE_VERSION);
setIsVisiblePage(isSupport);
};
useEffect(() => {
handleVisiblePage();
}, []);
// 아래부터 해당 서비스 비즈니스 로직
// ...
if (!isVisiblePage) return null;
return <div>신 서비스 메인 페이지입니다.</div>;
};
export default ServiceMainPage;
위 코드를 보면 ServiceMainPage 컴포넌트에서 현재 앱버전에 따라 서비스 페이지를 노출할 것인지 아닌지를 계산하는 로직과 서비스 메인의 비즈니스 로직이 혼재되어 있습니다.
이런 경우 두가지 단점이 있습니다.
- 서비스 메인의 비즈니스 로직을 수정하려면 앱버전에 따른 페이지 노출 여부 로직과 구분하여 살펴봐야 하는 번거로움
- 다른 서비스에서도 동일하게 사용할 수 있는 로직이므로 중복 코드 발생 우려
우리 회사는 앱 네이티브로부터 앱버전을 조회하는 로직, 현재 앱 버전이 서비스 지원 버전 이상인지 확인하는 로직, 페이지 노출 여부 플래그 상태값을 Custom Hook에 넣어서 재사용성을 높이고 관심사를 분리하여 다양한 서비스에서 활용하고 있습니다.
Custom Hook으로 관리하면 코드는 아래와 같이 깔끔해집니다.
// ServiceMainPage.tsx
import { useEffect, useState } from 'react';
import useNative from '@/hook/useNative';
const SERVICE_VERSION = '3.1.2';
const ServiceMainPage = (): React.ReactElement | null => {
const { isVisiblePage } = useNative(SERVICE_VERSION);
// 아래부터 해당 서비스 비즈니스 로직
// ...
if (!isVisiblePage) return null;
return <div>신 서비스 메인 페이지입니다.</div>;
};
앱버전에 따른 페이지 노출 여부 로직이 더이상 서비스 메인 페이지에는 없으므로 관심사가 어느정도 분리되었으며 다른 서비스에서도 해당 Custom Hook을 호출하여 앱버전에 따른 페이지 노출여부 로직을 재활용할 수 있는 이점을 챙길 수 있게 되었습니다.
한편, 저는 아래와 같은 이슈에 대해 고민해보았습니다.
이슈
- ServiceMainPage 컴포넌트 내부에는 아직 Custom Hook을 호출하는 코드와 페이지 노출 여부 상태값이 false인 경우 null을 반환하는 코드가 존재하여 관심사 분리가 더 필요하다고 생각
- 서비스 지원 버전 이하의 경우 페이지를 미노출(컴포넌트가 null을 반환)하는 것이 아닌 특정 UI를 보여주는 것으로 정책이 수정된다면 Custom Hook을 호출하는 모든 컴포넌트를 수정해야 하는 번거로움
첫번째 이슈의 경우 저는 팀원분들이 비즈니스 로직을 수정하게 된다면 해당 비즈니스 로직에만 집중할 수 있도록 관심사를 완벽히 분리하고 싶은 마음이 있었습니다.
두번째 이슈의 경우 하나의 정책 수정은 한 파일의 수정만 이루어지는 것이 테스트 비용을 절약할 수 있어 유지보수성을 높일 수 있다고 생각했습니다.
위 2개의 이슈를 해결할 수 있는 방법을 고민한 결과 HOC가 도움이 된다는 것을 알았습니다.
HOC(Higher Order Component)란?
컴포넌트 로직을 재사용하기 위한 React의 고급 기술
고차 컴포넌트(HOC)는 React API의 일부가 아니며, React의 구성적 특성에서 나오는 패턴
구체적으로, 고차 컴포넌트는 컴포넌트를 가져와 새 컴포넌트를 반환하는 함수입니다.
HOC의 예시 코드는 아래와 같습니다.
// WithHello.tsx
import { useState } from 'react';
interface WithHelloProps {
Component: React.ComponentType;
}
function WithHello({ Component }: WithHelloProps) {
const Hello: React.FC = (props) => {
const [isHello, setIsHello] = useState<Boolean>(false);
// 상태 값 업데이트 로직
// ...
return isHello ? <div>hello world!</div> : <Component {...props} />;
};
return Hello;
}
export default WithHello;
WithHello가 HOC이며 인자로 컴포넌트를 받아서 새로운 컴포넌트를 반환하는 컴포넌트라고 할 수 있습니다.
위 HOC의 원리를 이용하면 특정 조건에 따라 노출할 UI를 달리하고 싶은 경우 ServiceMainPage이 아닌 HOC에서 처리할 수 있으며 페이지 노출 정책이 바뀐다고 해도 하나의 HOC만 수정이 발생하여 수정 범위를 줄일 수 있습니다.
해결
위 HOC의 원리를 서비스에 적용하면 아래와 같이 리팩터링이 가능합니다.
// WithCheckAppVersion.tsx
import { useState, useEffect } from 'react';
interface WithCheckAppVersionProps {
supportVersion: string;
Component: React.ComponentType;
}
function WithCheckAppVersion({
supportVersion,
Component,
}: WithCheckAppVersionProps) {
const CheckAppVersion: React.FC = (props) => {
const [isVisiblePage, setIsVisiblePage] = useState<boolean>(false);
const isSupportVersion = (
curVersion: string,
supportVersion: string,
): boolean => {
// 현재 버전이 서비스 지원 버전 이상인지 확인하여 boolean을 반환하는 로직
// ...
};
const getCurVersion = async (): Promise<string> => {
// 네이티브로부터 현재 앱 버전을 전달 받아 해당 버전을 문자열로 반환하는 로직
// ...
};
const handleVisiblePage = async (): Promise<void> => {
const curVersion = await getCurVersion();
const isSupport = isSupportVersion(curVersion, supportVersion);
setIsVisiblePage(isSupport);
};
useEffect(() => {
handleVisiblePage();
}, []);
isSupportVersion ? <Component {...props} /> : null;
};
return CheckAppVersion;
}
export default WithCheckAppVersion;
// ServiceMainPage.tsx
import { useEffect, useState } from 'react';
import WithCheckAppVersion from '@/hoc/WithCheckAppVersion';
const SERVICE_VERSION = '3.1.2';
const ServiceMainPage = (): React.ReactElement | null => {
// 아래부터 해당 서비스 비즈니스 로직
// ...
return <div>신 서비스 메인 페이지입니다.</div>;
};
export default WithCheckAppVersion({
supportVersion: SERVICE_VERSION,
Component: ServiceMainPage,
});
ServiceMainPage 컴포넌트 내부에는 더이상 앱버전에 따른 페이지 노출 여부 로직이 없습니다.
HOC인 WithCheckAppVersion에서 현재 앱버전을 확인하여 특정 앱버전이하인 경우 페이지를 노출하지 않습니다.
만약 여기서 페이지를 미노출하고 팝업을 통해 앱 업데이트를 요구하는 정책이 추가된다면 어떻게 될까요?
// WithCheckAppVersion.tsx
// ...기존 로직
// 팝업 관리 Custom Hook
import useModal from '@/hook/useModal';
function WithCheckAppVersion({
supportVersion,
Component,
}: WithCheckAppVersionProps) {
const CheckAppVersion: React.FC = (props) => {
// ...기존 로직
// 팝업 띄우는 로직 추가
const showModal = useModal();
useEffect(() => {
if(!isSupportVersion) showModal('앱 업데이트가 필요해요!')
}, []);
isSupportVersion ? <Component {...props} /> : null;
};
return CheckAppVersion;
}
export default WithCheckAppVersion;
위처럼 HOC에 팝업을 띄우는 로직만 추가하면 HOC를 사용하는 모든 컴포넌트에 일괄 적용 가능하다는 이점을 가집니다.
또한, 위 페이지 노출 정책은 물론 변화하기 쉬운 로직을 HOC로 분리하여 수정 범위를 줄일 수 있는 이점이 있습니다.
마무리
HOC는 단순한 재사용 패턴뿐만 아니라 “변화하기 쉬운 로직”을 분리하는 설계의 출발점이 될 수 있다고 생각합니다.
페이지 노출 정책을 HOC에서 관리하고, 페이지를 렌더링하는 컴포넌트는 비즈니스 로직에 집중하도록 만들면 서비스 정책이 자주 달라져도 유지보수의 복잡도를 비교적 단순화할 수 있다는 것을 알았습니다.
'개발 여정' 카테고리의 다른 글
| 눈길을 달리는 JavaScript, TypeScript 스노우체인 장착기 (0) | 2025.09.14 |
|---|---|
| ApexCharts.js 대신 D3.js로 데이터 시각화하기 (0) | 2025.09.14 |
| 사내 라이브러리 표준화와 운영으로 로직 재사용성 높이기 (0) | 2025.03.02 |