Frontend
해외파트너센터 개발 오픈기 — 1. 이중 도메인에대한 접근 처리
윤주경Vana(바나) / 파트너웹개발팀여기어때
2024년 12월 5일
원문에서 보기 ↗
안녕하세요. 파트너웹개발팀에서 프론트 개발 업무를 하고 있는 바나입니다.
지난 6월에 여기어때 해외 파트너센터를 오픈했는데, 잠시 다른 프로젝트에 눈을 돌렸더니, 지금 블로그를 작성하는 이 계절은 벌써 ❄️겨울❄️이 되었네요!
이를 통해 해외 파트너들이 예약과 정산 내역을 확인할 수 있게 되었는데요, 정해진 시간 내에 파트너센터를 빠르게 구축할 수 있었던 과정을 공유하려고 합니다.
규모가 큰 파트너센터 사이트를 단기간 안에 오픈할 수 있을까? 🤔
파트너센터는 여기어때에서 판매되고 있는 숙박시설(호텔, 펜션, 게스트하우스, 홈&빌라)의 사장님들이 예약 관리와 판매 관리, 리뷰 관리 등을 할 수 있는 관리 사이트 입니다. 저 메뉴 외에도 정산 내역과 특가 관리나 데이터 분석 등 많은 기능을 제공하는 파트너웹개발팀이 개발하고 운영하는 주요 서비스 입니다.
최근 여기어때가 일본 시장에 진출함에 따라 해외 파트너사의 고객들이 우리 파트너 센터의 기능을 이용할 수 있도록 제공되어야 했습니다. 고객들이 사용할 페이지는 예약 내역/정산내역 말고도 로그인 페이지, 2차 인증, 마이페이지 등등 부수적인 화면들도 필요했고 이후에 점차 국내 파트너 센터와 같이 확장될 여지가 있었기 때문에 새롭게 만드는 것은 비효율적이란 판단을 하게 되었습니다.
우리가 기존에 운영해오던 파트너센터는 신규 개발 당시 착수부터 종료까지 약 1년간 10여명의 인원이 지속적으로 참여한 프로젝트였습니다. 그리고 우리만의 디자인 시스템을 처음으로 도입하고 적용한 프로젝트였기 때문에 변화된 업무 환경과 협업 프로세스에 적응해 가면서 진행하는 것 또한 낯선 일이었습니다. 이 당시를 떠올리며 저는 같은 프로젝트를 처음부터 한 번 더 하는 것은 쉽지 않을 것이라는 걱정도 있었습니다.
그리하여 가지고 있는 컴포넌트들은 그대로 활용하며, 페이지만 해외용으로 쓴다면 일정을 많이 줄일 수 있을 것 같았습니다. 그래서 국내 파트너센터의 레포지토리 안에서 해외 직계약 숙소용 파트너 센터를 구축하는 방법으로 진행했습니다.
어떻게하면 기존 파트너센터와 다르게 접근할 수 있을까? 🤔
로그인 페이지 화면부터 호출하는 API도 다르기 때문에 해외 사용자들에게는 oversea.goodchoice.kr 서브도메인으로 접속하고 해외용 파트너센터 화면으로 유도했습니다. 기존 프로젝트는 Next.js로 되어있었기 때문에 페이지 렌더링 전에 실행하는 Next.js의 Middleware를 통해 Routing 했습니다.
이후에 나오는 파트너센터의 코드는 다음과 같은 스펙을 참고하시면 됩니다.
- Next.js
- Recoil (상태관리)
- i18next (다국어 처리)
middleware.js
import { NextResponse } from 'next/server';
export function middleware(request) {
const { pathname, search } = request.nextUrl;
const locale = request.cookies.get('i18next')?.value || 'ko';
const host = request.headers.get('Host');
const isOversea = host?.indexOf('oversea') > -1 || locale !== 'ko';
//...생략
function tokenMiddleware(request, isOversea) {
const { pathname, search } = request.nextUrl;
let simplePathname = pathname?.replaceAll('/en', '');
let resUrl = simplePathname + search;
if (로그인 판단) {
// 로그인 했을 경우
if (토큰이_필요한_URL_목록.includes(simplePathname)) {
resUrl = '/';
}
} else {
// 로그인 안했을 경우
if (!토큰이_필요하지_않는_URL_목록.includes(simplePathname)) {
const returnUrl;
resUrl = `/login${returnUrl}`;
}
}
if (isOversea && !resUrl.startsWith('/en')) {
resUrl = `/en${resUrl}`;
}
if (수정이 된 url인 경우) {
return NextResponse.rewrite(new URL(resUrl, request.url)); // 다시 rewrite로 middleware를 타도록 함
}
return NextResponse.next();
}
위의 코드는 로그인 여부에 따라 이후에 사용자에게 제공되는 페이지가 달라지는 기능을 담당합니다. 로그인 여부와 국내/해외 판단도 같이 들어있습니다.

해외용 파트너센터에서 제공하는 페이지들은 pages 폴더 안에 하위 폴더 “/en”을 생성해 경로를 추가하였습니다.
oversea 서브 도메인으로 들어오는 url과 ‘/en’경로가 포함되어 있는 url도 모두 해외 파트너센터 페이지로 이동을 시켜 주어야 하기 때문에 기본 path 값만 비교하여 해외/국내를 판단하고 그에 맞는 새로운 url로 변환하여 rewrite했습니다. 그리고 체크 이후 변환된 url로 들어올 때는 해당 페이지로 정상 이동할 수 있도록 NextResponse.next()를 사용해 라우팅 했습니다.
해외 파트너 센터에서 보이는 메뉴는 국내와 달라요! 😲
해외 파트너센터에서 사용하는 메뉴들은 국내와 차이가 있고 사용하는 API도 다릅니다. 그렇기 때문에 전역적으로 해외인지 국내인지를 판단하는 값이 필요했습니다. 다국어 처리를 위해 i18next 라이브러리를 사용했고 i18next 초기화 시 도메인 값으로 판단하여 등록되는 locale 값을 이용해 isOversea 값의 상태를 관리했습니다.
locale.js
import { atom } from 'recoil';
import { nanoid } from 'nanoid';
export const locale = atom({
key: nanoid(),
default: 'ko',
});
useLocale.js
import { useRecoilState } from 'recoil';
import { useEffect, useState } from 'react';
import { LOCALE_KEY } from '@constants/appBridge'; // "i18next" 값을 상수로 관리
import useCookie from '@/hooks/storage/useCookie';
import { locale } from '@/store';
function useLocale() {
const { getCookie } = useCookie();
const initOverseaData = getCookie(LOCALE_KEY) === 'en';
const [userLocale, setLocale] = useRecoilState(locale);
const [isOversea, setIsOversea] = useState(initOverseaData);
useEffect(() => {
// 초기 언어 세팅
const i18nextLng = localStorage.getItem('i18nextLng'); // 값이 없는 경우 'ko'
const isOversea = location.host.indexOf('oversea') > -1;
const lngCode = isOversea ? 'en' : i18nextLng;
setLocale(lngCode);
}, []);
useEffect(() => {
setIsOversea(userLocale !== 'ko');
}, [userLocale]);
return {
isOversea,
userLocale,
setLocale,
};
}
export default useLocale;
이렇게 어디서든 isOversea에 대한 값을 가져와서 사용할 수 있도록 hook으로 만들어두었습니다. 그럼 컴포넌트나 다른 곳에서도 국내인지 해외인지 판단하여 그에 맞는 디자인 화면과 처리를 할 수 있겠죠?
Layout/index.js
function Layout({ pageInfo, children }) {
...
const { isOversea } = useLayout();
const { layout } = pageInfo;
return (
<Wrapper>
{layout === 'default' && <Lnb />}
<main>{isOversea ? <OverseaLayout pageInfo={pageInfo}>{children}</OverseaLayout> : <LocalLayout pageInfo={pageInfo}>{children}</LocalLayout>}</main>
</Wrapper>
);
}
Layout.displayName = 'Layout';
가장 먼저 화면을 그리는 레이아웃 단계에서 isOversea 값으로 국내/해외를 판단하여 Layout 컴포넌트를 불러옵니다.
코드 내에 isOversea가 너무 많이 사용되는 거 같아요 🥲
이 외에도 디자인이 다른 화면에서 쓰는 부분들이 있는 곳마다 isOversea 값으로 판단하는 부분들이 많아지면서 공통으로 쓰는 컴포넌트마다 isOversea 값으로 처리하는 부분이 많아졌습니다. 예를 들어 메뉴 컴포넌트는 국내와 해외 양쪽에서 사용되는 컴포넌트로 이를 살펴보면,
function WebLnb({ items, pageInfo, onLnbLink, menuInfo }) {
const { t } = useTranslation();
const { isOversea } = useLocale();
return (
<Wrapper $isOversea={isOversea}>
<div>
{return (
<MenuWrap key={index} $selected={index === mainId}>
{item.subMenu && (
<SubMenu>
<SubItem onClick={() => onLnbLink()}>
{t(subItem.label)}
{subItem?.badge}
</SubItem>
</SubMenu>
)}
</MenuWrap>
)}
</div>
<EtcWrap>
{isOversea ? (
<HelpCall />
) : (
<>
<Link href={RESEARCH_LINK} target="_blank">
<ResearchItem>
<Icon name="icn_chat" width="20px" height="20px" />
<div>
<Tags type="purple" size="xs">
최대 10만 포인트 지급
</Tags>
리서치 참여 신청
</div>
</ResearchItem>
</Link>
</>
)}
</EtcWrap>
</Wrapper>
);
}
export default WebLnb;
이렇게 isOversea 값을 활용하여 해외 여부를 판단하고, 그에 따라 사용하지 않는 컴포넌트를 구분하고, 스타일도 처리한 것을 볼 수 있습니다. 이 경우, 만약 국내와 해외를 분리해서 처리할 일이 생긴다면, 코드 안에 있는 isOversea로 분기 처리된 부분을 일일이 살펴야하기 때문에 한눈에 파악하기 어렵습니다. 그래서 간편한 유지보수를 위해 이 컴포넌트를 완벽하게 분리해야한다고 판단했습니다.

폴더 구조
Footer, Header, Layout, LNB 등 공통으로 사용하는 컴포넌트를 관리하는 방법을 개선하여 코드에서 일일이 분석하는 시간을 줄일 수 있게 되었습니다. 이 개선안은 컴포넌트 내부에서 처리하지 않고, 경로부터 Local과 Oversea를 분리하는 방법이었습니다. 그리고 함께 사용하는 hook은 isOversea 값을 전달받아 이 기능을 처리하게 개선하였습니다. (멋있게 리팩토링해 주신 타비, 죠니 감사합니다🙇♀️👍)
function useLNB({ isOversea = false }) {
useEffect(() => {
setLnbInfo((prevState) => ({
...prevState,
menuList: isOversea ? OVERSEA_MENU_LIST : MENU_LIST,
}));
}, [isOversea]);
}
export default useLNB;
이로써 추후에 해외 파트너센터의 화면이 국내와 다르게 변경된다고 하더라도 개발자는 양쪽 소스코드를 분석해야하는 부담 없이 수정할 수 있게 되었습니다!
앞으로 더 많은 기능이 추가되거나 변경될 경우에 이 방법을 계속 사용한다면 관리하는 파일이 많아집니다. 그렇다면 나중에는 프로젝트를 분리하는 것도 좋은 방법이 될 수 있겠네요.…🥹 여전히 더 좋은 방법이 있을까 고민하는 파트너웹개발팀입니다! (아자아자!)
긴 글 읽어주셔서 감사합니다~
다음 글에서는 민티께서 해외 파트너센터에서 새롭게 추가된 이메일 인증 공통 서비스 개발 이야기를 다뤄주실 예정입니다.
앞으로 해외 갈 때도 여기어때 많이 이용해 주세요💜