올리브영 테크블로그 포스팅 프레임워크에 구애받지 않는 통합 백오피스 구축하기
Tech

프레임워크에 구애받지 않는 통합 백오피스 구축하기

'Vue 웹 컴포넌트'로 React, JSP, Vue에 어울리는 만능 양념장 레시피 만들기

2026.07.14



들어가며

안녕하세요! 올리브영에서 카탈로그 플랫폼 서비스를 담당하는 팀에서 프론트엔드 개발을 맡고 있는 몽이형입니다.

혹시 올리브영 기술 블로그의 마이크로프론트엔드(MFE) 시리즈(2025.07~11 발행, 총 3부작)를 보셨나요? 모노레포와 iframe 통합 이슈(2025.12 발행)를 대하는 올리브영의 관점을 잘 보여주는 주제였는데요. 이번에는 조금 다른 관점에서 접근합니다. iframe의 격리(Cross-Origin)로 인한 한계를 어떻게 벗어났는지, 그리고 부모의 콘텍스트를 그대로 흡수하면서도 구조적 보안을 어떻게 확보했는지에 대한 이야기입니다. 이 이야기는 아래 질문으로부터 시작됩니다.


👩🏻‍💻 "화면 UI 하나 바꾸려는데 React, Vue, JSP 코드를 각각 다 수정해야 한다고요? 진짜 방법이 없을까요?"


프론트엔드 개발자라면 멀티 프레임워크 환경에서 한 번쯤 이런 깊은 고뇌에 빠져보셨을 겁니다. 저희 팀 역시 올리브영의 상품과 연관된 수많은 내·외부 백오피스를 운영하며 같은 문제를 겪고 있었습니다. 각 시스템이 React/Next부터 Vue, 그리고 레거시인 JSP까지 저마다의 프레임워크로 개발되어 있었기 때문이죠. 하지만 저는 모든 프레임워크에 넣어도 어울리는 '만능 양념장'을 발견했습니다. 배포 없이도 실시간으로 전 채널의 화면을 조화롭게 버무린 맛깔나는 개발 여정을 지금부터 본격적으로 공유해 보겠습니다.


🔪 혼돈의 레시피: 서로 다른 프레임워크에 같은 화면 얹기?

저희 팀은 올리브영 상품과 연관된 백오피스 개발을 담당합니다. 그중 협력사가 사용하는 상품 관리 시스템(Vue, React)과 품질 관리 시스템(JSP)을 함께 운영하고 있는데요. 어느 날, 3가지 시스템에 동일한 상품정보 화면을 제공해야 하는 상황이 주어졌습니다. 동일한 API를 바라보고 동일한 목적을 가진 화면인데, 단지 프레임워크가 다르다는 이유로 같은 UI와 로직을 최대 세 벌씩 새로 만들어야 했죠. 프레임워크라는 벽에 가로막혀 같은 화면을 세 번씩 중복 개발해야 했던 아찔한 상황. 그야말로 비효율의 극치였습니다.

저희에게는 기존 시스템을 뒤엎지 않으면서도, 공통된 화면을 완성해 줄 '치트키'가 간절했습니다. 바로 '라면 스프'나 '만능 양념장' 같은 존재를 말이죠. 그리고 마침내 찾아내고야 말았습니다. 바로 그 녀석을...! 그 이름은 바로... 웹.컴.포.넌.트.




🥫 비법 소스 찾기: 대안 솔루션 비교 분석

그런데 어떤 과정을 거쳐 웹 컴포넌트라는 보편적이지 않은 방식에 주목을 하게 된 것일까요? 저희도 처음에는 문제 해결을 위해 흔히 쓰이는 전통적인 방식들을 검토해 보았습니다. 그러나 저마다 치명적인 한계가 있었지 뭡니까?!

iframe vs 시스템 마이그레이션 vs Vue 웹 컴포넌트

결론부터 말하면, iframe과 시스템 마이그레이션 모두 각자의 방식으로 한계에 부딪혔습니다. iframe은 Cross-Origin 보안 정책 때문에 부모 창과 쿠키·세션을 공유하지 못합니다. 이미 로그인된 React 화면 위에 다른 시스템 화면을 iframe으로 얹으면, 사용자는 방금 로그인했는데도 iframe 내부에서 또 한 번 로그인을 요구받는, 이른바 '로그인 지옥'에 빠지게 됩니다.

시스템 마이그레이션은 화면 하나를 공유하자고 수백 개의 기능을 통째로 재작성해야 하는, 주객이 전도되는 선택이었습니다. 3가지 시스템을 Vue 하나로 통일하려면 JSP로 짜인 레거시 시스템 전체를 다시 짜야 하고, 수개월의 작업 기간을 쏟아야 하는 데다, 그 과정에서 멀쩡히 돌던 기존 시스템의 안정성까지 흔들릴 리스크를 떠안게 되죠.

마침 Vue는 defineCustomElement(Vue 컴포넌트를 브라우저 표준 커스텀 태그로 변환하는 Vue 3 API)를 자체 지원하고 있어서, 모던 프레임워크로도 웹 컴포넌트를 빠르게 구축할 수 있었습니다.


항목 iframe 시스템 마이그레이션 Vue 웹 컴포넌트 ✅
기본 메커니즘 타 시스템 화면을 단순 임베드 하나의 프레임워크로 통일
Vue로 개발, 표준 기술 기반 커스텀 태그화
핵심 장점 가장 단순함, 화면 독립 배포 가능 아키텍처의 순수성 및 통일성 확보
프레임워크 종속 없음, 부모 자원 공유, 화면 독립 배포 가능
결정적 단점 보안/인증 및 세션 공유 차단 (SameSite) 수 개월의 대공사, 배보다 배꼽이 큰 리소스 낭비
초기 모노레포 및 빌드 파이프라인 구성 필요

정리하면, 웹 컴포넌트는 프레임워크에 종속되지 않으면서도 부모 시스템의 인증·자원을 그대로 공유하고, 화면 단위로 실시간 독립 배포까지 가능한 기술입니다. 그렇습니다. 웹 컴포넌트가 만능 양념장이었던 것입니다!

웹 컴포넌트님, 생존하셨습니다 ^^



📔 조화로운 레시피: 아키텍처 설계

만능 양념장으로 웹 컴포넌트를 선택했다면, 이제는 어떤 방식으로 활용할 것인지 고민을 해야 했는데요. 저희는 웹 컴포넌트의 범용성과 모노레포(Monorepo)의 효율성을 결합한 'BackOffice Standard System' 모델을 설계했습니다.

BackOffice Standard System Architecture
BackOffice Standard System Architecture

BackOffice Standard System 컨셉

모노레포에서 생성된 각각의 메뉴 컴포넌트가 'Vue 웹 컴포넌트' 형태로 빌드되어, CDN을 통해 어떤 웹 환경(React, Vue, JSP 등)에서든 커스텀 태그 형태로 자유롭게 로드되는 구조


이 아키텍처는 다음과 같은 프로세스를 거쳐 화면을 그려냅니다. 요약하자면, 모노레포로 관리/개발된 한 벌의 코드를 웹 컴포넌트로 빌드하여 여러 채널에 CDN 형태로 서빙하는 레시피라는 것입니다.


1. 재료 관리: 모노레포를 통한 공통 코드 통합 관리

여러 시스템에 흩어질 수 있는 상품 관리 메뉴를 단 하나의 모노레포 안에서 독립된 패키지로 관리합니다. 소스코드는 한 곳에서 표준화되어 관리되므로 중복 코드가 극적으로 감소하고 유지보수 효율성도 크게 높아집니다.

2. 양념 제조: 웹 컴포넌트 빌드

Vue로 개발된 패키지들을 프레임워크에 종속되지 않는 독립된 순수 JS 파일로 가공합니다.

3. 서빙: S3 & CDN

빌드된 JS 파일을 S3에 업로드하고 CDN과 연결하여, 브라우저가 정적 자원을 가장 빠르게 다운로드할 수 있는 징검다리를 만듭니다.

4. 제공: 화면 렌더링

각 서비스들은 프레임워크가 React든, Vue든, 심지어 레거시 JSP든 상관없이 그저 이 CDN URL을 <script> 태그로 불러오기만 하면 됩니다. 그 후 화면에는 일반 HTML 태그를 선언하듯 커스텀 태그(예: <goods-detail-component>)를 넣고 각 환경에 맞게 공통으로 작성한 렌더링 로직을 적용하면 화면 렌더링이 완료됩니다.


각기 다른 서비스에 통합되는 '상품조회' 공통 화면 플로우
각기 다른 서비스에 통합 적용되는 '상품조회' 공통 화면 플로우 적용 예시

👨‍🍳 주방 세팅하기: 상세 구현 및 코드 가이드

그렇다면 내부 코드는 어떻게 구성되어 있을까요?


1. React 환경에서의 탑재

React가 부모 시스템이라면, 아래와 같이 미리 작성된 커스텀 훅을 활용해 단 몇 줄만으로 만능 양념장인 웹 컴포넌트를 얹을 수 있습니다.

import WcScript from '@components/web-component/WcScript'
import { useWebComponent } from '@hooks/useWebComponent'
...

const GoodsDetailPage = () => {
  const component = useRef(null)
  const { isDefined } = useWebComponent({ ref: component, pageName: 'goods-detail' })

  return (
    <Spin isLoading={!isDefined} ... >
      <WcScript />
      <goods-detail ref={component} />
    </Spin>
  )
}

export default GoodsDetailPage


2. 커스텀 훅 작성

웹 컴포넌트를 화면에 탑재하기 위한 커스텀 훅 코드의 일부입니다. 핵심은 Shadow DOM(웹 표준 기술로, 특정 요소의 스타일·구조를 전역 DOM과 분리하는 캡슐화 영역) 위에서 동작하는 웹 컴포넌트에 스타일을 잘 입히는 것이에요. 아래 코드를 보기 전에, 앞으로 자주 등장할 핵심 개념 하나만 먼저 짚고 가겠습니다.

Shadow DOM

웹 표준 기술의 하나로, 특정 요소의 마크업 구조와 스타일을 전역 문서(DOM)로부터 완벽히 분리하여 "독립적인 캡슐화 영역"을 생성하는 인터페이스입니다. 이 캡슐의 진입점 역할을 하는 객체를 ShadowRoot라고 부릅니다.


import { useEffect, useState } from 'react'
...

export const useWebComponent = ({ ref, pageName, fileName, option }: Params) => {
  ...
  const [isDefined, setIsDefined] = useState(false)

  const moveVueStylesIntoShadow = (shadowRoot: ShadowRoot) => {
    const globalStyles = Array.from(document.querySelectorAll('style'))

    globalStyles.forEach((styleEl) => {
      const isVueStyle = styleEl.textContent?.includes('[data-v-')

      if (isVueStyle && !shadowRoot.contains(styleEl)) {
        const cloned = styleEl.cloneNode(true)

        shadowRoot.appendChild(cloned)
      }
    })
  }

  const getWebComponentCss = async () => {
    let wcStyle = ''
    const CSS_URL = `...`

    try {
      const response = await fetch(CSS_URL)

      if (!response.ok) {
        throw new Error(`CSS 로드 실패: ${response.status} ${response.statusText}`)
      }

      wcStyle = await response.text()
    } catch (e) {
      console.error('CSS 로드 에러:', e)
      throw e
    }

    return wcStyle
  }

  const injectStyleToComponent = async (shadowRoot: any) => {
    const style = document.createElement('style')
    style.textContent = styleContent
    shadowRoot.prepend(style)

    const wcStyle = document.createElement('style')
    wcStyle.setAttribute('data-wc-style', 'true')
    wcStyle.textContent = await getWebComponentCss()
    shadowRoot?.appendChild(wcStyle)
  }

  useEffect(() => {
    const el = ref.current

    if (!el) {
      return
    }

    async function apply() {
      await customElements.whenDefined(pageName).then(async () => {
        const component = ref.current

        if (component?.shadowRoot) {
          await injectStyleToComponent(component?.shadowRoot).catch(console.error)
          moveVueStylesIntoShadow(component?.shadowRoot)
        }

        setIsDefined(true)
      })
    }

    apply()
  }, [pageName, ref])

  return { isDefined }
}

const styleContent = `...`.trim()

이렇게 보니 커스텀 훅 예시는 조금 길고 복잡해 보이는데요. 핵심 재료들을 하나씩 맛보면 어렵지 않습니다.


(1) moveVueStylesIntoShadow — 전역에 흩어진 Vue 스타일 끌어모으기

Vue 웹 컴포넌트는 빌드 시 스타일을 전역 document<style> 태그로 주입합니다. 하지만 컴포넌트는 ShadowDOM이라는 독립된 캡슐 안에서 동작하기 때문에, 전역에 있는 스타일이 그대로 적용되지 않아요. 그래서 [data-v-로 시작하는 Vue 전용 스타일만 골라내어(isVueStyle) ShadowDOM 안으로 복제해 넣어줍니다.

(2) getWebComponentCss — CDN에서 빌드된 CSS 가져오기

컴포넌트 빌드 결과물에 포함된 CSS 파일을 CDN URL에서 fetch로 직접 받아옵니다. 응답이 정상이 아니면(!response.ok) 에러를 던져 스타일 누락 상황을 명확히 잡아냅니다.

(3) injectStyleToComponent — ShadowDOM에 스타일 입히기

앞에서 준비한 두 종류의 스타일을 ShadowDOM에 실제로 주입하는 부분입니다. 기본 스타일(styleContent)은 prepend로 가장 앞에 깔고, CDN에서 받아온 웹 컴포넌트 CSS는 data-wc-style 식별자를 붙여 appendChild로 뒤에 얹습니다. 순서가 곧 우선순위이기 때문에, 이 부분은 주의해서 적용합니다.

(4) useEffect — 컴포넌트가 준비되면 스타일 버무리기

가장 중요한 타이밍 제어입니다. customElements.whenDefined(pageName)(해당 이름의 커스텀 태그가 브라우저에 등록·사용 가능해질 때까지 기다려주는 웹 표준 API)로 커스텀 태그가 브라우저에 정의될 때까지 기다린 뒤, shadowRoot가 생성되면 비로소 3번의 스타일 주입과 1번의 Vue 스타일 이동을 실행합니다. 모든 준비가 끝나면 setIsDefined(true)로 로딩 상태를 해제해, 부모 화면이 깜빡임 없이 완성된 컴포넌트를 보여줄 수 있게 합니다.


어때요? 정말 쉽~죠?


🤖 자동 급식: 시스템 배포가 필요 없는 '독립 배포'의 실현

이 아키텍처에서 주목해야 할 특징 중 하나는 부모 시스템의 재배포가 전혀 필요 없다는 점인데요. 공통 화면에 수정 사항이 생기면 모노레포에서 빌드하여 CDN에 연결되어 있는 S3 내 js 파일만 새로 덮어쓰면 반영이 완료됩니다. 부모 시스템은 어떠한 재빌드나 배포 과정도 필요 없이, 사용자가 새로고침을 하는 순간 갱신된 CDN 자원을 받아 가기 때문에 실시간으로 전 채널에 반영되어 새 화면을 즉시 맛볼 수 있습니다. (단, 이는 캐시 버스팅 등 최신 자원을 보장하는 조치가 함께 적용되어 있을 때 성립합니다. 자세한 내용은 바로 아래에서 다룹니다.)


잠깐! 정말 새로고침만으로 최신 화면이 보장될까?

사실 그렇지 않습니다. CDN 엣지 노드는 Cache-Control: max-age 값에 따라 파일을 캐싱하기 때문에, 파일을 덮어써도 TTL이 만료되기 전까지는 새로고침을 해도 구버전이 그대로 응답될 수 있습니다. 저희는 이 문제를 CDN 요청 URL에 현재 날짜·시간을 쿼리 파라미터로 붙이는 방식(cache busting)으로 해결했습니다. 요청 주소가 매번 달라지므로 엣지 노드가 이를 새로운 자원으로 인식해 항상 최신 파일을 내려주게 됩니다. 이 외에 파일명에 content-hash를 넣거나, 배포 시점에 CDN Invalidation을 호출하는 방법도 함께 고려할 수 있습니다.



만능 양념장으로 만든 신메뉴를 자동 급식으로 제공해 준다고..?!?! 미.쳤.다 ( positive )


🍽️ 아무나 먹어도 탈 안 나는 치트키: iframe은 못 했지만 웹 컴포넌트는 해낸 보안 레이어

그런데 이렇게 만능으로 여기저기 막 갖다 붙여도 되는 것일까요? 여기서 주목할 핵심은, 잘 구성된 웹 컴포넌트는 CDN 단독 접근만으로는 민감 데이터에 접근할 수 없도록 실제 인증 정보를 런타임에 부모 시스템이 주입하는 구조라는 점인데요. 웹 컴포넌트는 부모 서비스의 내부 스크립트 엔진 위에서 직접 실행되므로 부모의 자원을 그대로 공유받습니다. 환경에 따라 다음 3가지 인증 시나리오를 유연하게 수용합니다.

1. 쿠키/헤더 토큰 인증 시스템

웹 컴포넌트가 부모 시스템과 같은 오리진(내부 코드)에서 실행되므로, iframe과 달리 SameSite로 인한 쿠키 차단 없이 기존 인증 체계를 그대로 활용할 수 있습니다. 덕분에 별도의 복잡한 연동 설정이 필요 없습니다.

다만, 쿠키 기반 인증은 SameSite 설정 값에 따라 동작이 달라지고, 브라우저가 쿠키를 자동으로 실어 보내는 특성상 CSRF 방어(토큰·Origin 검증 등)는 부모 시스템 차원에서 별도로 갖춰져 있어야 합니다. 이 시나리오는 "부모와 동일 오리진에서 실행된다"는 전제에서만 위와 같이 매끄럽게 성립합니다.

2. 특정 API 인스턴스 주입

부모 시스템이 API를 호출할 때 사용하는 Axios 인스턴스 자체를 웹 컴포넌트의 프로퍼티로 통째로 넘겨받습니다. 웹 컴포넌트는 이 인스턴스를 받아 그대로 호출하기만 하면 되므로, 이미 헤더에 세팅된 인증 정보가 자동으로 녹아들게 됩니다.

3. 이종 도메인 API 호출

서버 간 약속된 토큰을 프로퍼티로 넘겨받아, 웹 컴포넌트 내부에서 헤더에 동적으로 주입하여 연동합니다. 웹 컴포넌트는 부모 시스템으로부터 프로퍼티로 토큰과 설정에 필요한 정보를 넘겨받은 후, 헤더에 세팅하여 인증 정보가 안전하게 반영되도록 처리합니다.



안전함의 비밀 (Structural Security)

CDN에 배포된 JS 파일은 그저 껍데기뿐인 화면 렌더링 로직일 뿐입니다. 실제 데이터를 움직이는 핵심 key나 Axios 인스턴스는 오직 부모 시스템의 메모리 상에서만 동적으로 주입됩니다. 특히 Axios 인스턴스 주입 방식(시나리오 2)에서는, 누군가 악의적으로 저희 CDN 주소를 탈취해 본인들 페이지에 강제로 임베드하더라도 부모 인스턴스가 없이는 그 어떤 데이터에도 접근할 수 없습니다.


🧂 만능에 더 가깝게: 완성도 높이기

여기까지 오면 "정말 만능인가?" 싶으실 텐데요. 결론부터 말하면, 아래 몇 가지 특성만 이해하고 대비해 두면 웹 컴포넌트는 정말 만능 양념장에 가깝게 활용할 수 있습니다. 저희가 도입하며 챙긴 포인트들을 공유합니다.

1. Shadow DOM 스타일 격리 → 한 번에 잡아두기

Shadow DOM이 스타일을 완벽히 격리해 주는 덕분에, 어떤 부모 시스템에 얹어도 스타일이 서로 충돌하지 않는다는 것이 오히려 큰 강점이에요. 다만 전역 스타일이 캡슐 안으로 들어오지 못하므로, 본문에서 살펴본 moveVueStylesIntoShadow·injectStyleToComponent 같은 스타일 주입 로직을 한 번만 만들어 두면 됩니다. 초기에 이 공통 훅만 잘 갖춰 두면 이후에는 거의 신경 쓸 일이 없습니다.

2. SSR/SEO보다는 '로그인 이후' 적용하기

웹 컴포넌트는 브라우저에서 커스텀 태그가 정의된 뒤에 렌더링되는 특성상, SSR이나 검색 엔진 노출이 핵심인 공개 화면보다는 로그인 뒤에서 동작하는 백오피스·어드민 성격의 화면에 특히 잘 맞습니다. 저희의 적용 대상이 정확히 그 영역이었기에 이 특성은 전혀 걸림돌이 되지 않았습니다.

3. 초기 CDN 로드 지연은 로딩 처리로 매끄럽게

컴포넌트 스크립트를 CDN에서 내려받아 커스텀 태그가 정의되기까지 아주 짧은 지연이 있는데요. 본문의 isDefined 로딩 처리로 이 순간의 깜빡임을 자연스럽게 가려 두면, 사용자는 지연을 거의 체감하지 못합니다.

4. Vue 버전은 한곳에서 관리 → 오히려 통제가 쉽다

defineCustomElement의 동작은 Vue 버전에 따라 조금씩 달라질 수 있지만, 모든 채널이 하나의 모노레포에서 빌드된 공통 컴포넌트를 소비하기 때문에 버전을 한곳에서 일괄 관리할 수 있습니다. 업그레이드 시 회귀 테스트만 한 번 잘 거치면 전 채널에 안전하게 반영되므로, 파편화된 환경보다 오히려 버전 통제가 수월합니다.


마무리하며

이 경험을 바탕으로, 저희는 파편화된 백오피스 환경을 안전하고 끈끈하게 통합하는 데 성공했습니다. 물론 이 결정을 내리고 실제 운영환경에 적용하는 과정이 마냥 순탄치만은 않았습니다. 사실 웹 컴포넌트는 현재 프론트엔드 생태계에서 그리 보편적으로 사용되는 기술이 아니었기에, 수많은 고민과 의구심이 있었기 때문이죠.

하지만..! BUT!

개인적으로 이 방식이 문제를 해결하는 '최고의 정답'이었다고 단언할 수는 없으나, 당시 저희 팀이 처한 환경과 제약 속에서는 가장 '최선의 선택'이었다고 생각합니다. 또한 단순히 화면 한 벌을 공유하는 것을 넘어, 기존 레거시 시스템을 리스크 없이 점진적으로 모던화할 수 있는 발판을 마련했다는 점이 이번 프로젝트의 가장 큰 수확이 아닐까 싶습니다.

앞으로도 저희 팀은 비즈니스의 빠른 성장을 뒷받침하면서도 개발 생산성과 보안을 모두 챙길 수 있는 탄탄한 Front-End 아키텍처를 계속해서 고민하고 발전시켜 나가겠습니다. 저희 팀의 다음 레시피가 궁금하시다면, 올리브영 기술 블로그를 계속 주목해 주세요! 긴 글 읽어주셔서 감사합니다.


함께 읽어보기

웹 컴포넌트가 낯설게 느껴지셨다면, 이미 이 기술을 프로덕션에서 활용하고 있는 아래 사례들이 좋은 참고 자료가 될 거예요. 특히 아래 해외 주요 기업들이 비슷한 문제의식으로 웹 컴포넌트를 어떻게 활용했는지 살펴보시길 추천합니다.


Frontend-ArchitectureVueWebComponent
올리브영 테크 블로그 작성 프레임워크에 구애받지 않는 통합 백오피스 구축하기
🐾
몽이형 |
Front-end Engineer
몽이 귀여워..