IE 격리공간을 만들어보자

권세규
네이버 플레이스 개발 블로그
12 min readAug 23, 2022

--

2022년 6월 15일, 웹 생태계에 역사적인 순간이 탄생했습니다. 바로 인터넷 익스플로러(IE) 지원 종료가 공식적으로 적용되는 날이었습니다. IE는 모던 JavaScript 및 CSS 스펙을 지원하지 않아, 개발 경험을 저해하고 웹 생태계의 발전을 방해해 왔습니다.

Glace CIC에서 개발하는 서비스 중 하나인 스마트플레이스 역시, IE로 인해 비용을 감수해 왔습니다.

스마트플레이스는 Next.js를 기반으로 개발하였는데, 22년 2월 12 버전으로 업그레이드를 시도하였을 때, IE 관련 이슈를 겪었습니다. 이는 canary 버전에서 금방 고쳐졌지만, 다른 불안정한 모습이 보여 결국 무산되었습니다. 이전에도 다룬 적이 있는 CSS Custom Variable 이슈 역시, IE가 모던 CSS 문법을 지원하지 않았기 때문이었습니다.

마이크로소프트의 지원 종료는 곧 스마트플레이스의 IE 대응 종료로 이어졌습니다. 그러나 단번에 IE 대응 장치를 없앨 수는 없었습니다. 소수긴 하지만 여전히 IE로 들어오는 사용자가 존재했거든요. 아무리 사전 공지를 했다 하더라도, 갑작스럽게 하얀 화면을 마주한다면 굉장히 실망스러울 것입니다.

따라서 스마트플레이스 기획에서는 아래와 같은 안내 화면을 노출하기로 결정하였습니다.

스마트플레이스의 최신 브라우저 업데이트 안내 화면
스마트플레이스의 최신 브라우저 업데이트 안내 화면

이 문제를 풀어나가려면 어떻게 해야 할까요? 지금은 IE만 지원 종료가 되었지만, 추후에 동일한 일이 벌어질 수 있지 않을까요? 저는 다음과 같은 단계로 문제를 쪼개기로 했습니다.

  1. IE로 접속 시, 브라우저 업데이트 안내 페이지(ie-update)로 리다이렉트
  2. ie-update를 Next.js에서 격리, 별도 구역에서 서빙하기

1. IE로 접속 시, ie-update로 리다이렉트

HTTP 헤더에는 User-Agent라는 항목이 있습니다. 이것은 서버로 들어온 HTTP 요청의 주체가 어떤 장치나 OS를 사용하였는지 기술하는 변경 불가능한 헤더입니다. MDN에서 언급하듯이, 이것으로 브라우저 구분을 하는 것은 신중하게 결정해야 합니다.

그러나 IE는 더이상 업데이트가 되지 않기 때문에, User-Agent에 표시되는 내용은 불변입니다. IE 버전에 따라 조금씩 다르지만, 아래와 같은 문자열을 포함하는 것이 알려져 있습니다.

IE10- MSIE
IE11 Trident/7.0

그렇다면 서버에서 HTTP 헤더를 보고, IE인 경우에는 해당 페이지로 리다이렉트하면 됩니다. 여기까지만 들으면 굉장히 간단해 보입니다.

ie-update가 아니라고 전부 리다이렉트를 해도 될까?

Next.js에는 자체적인 리다이렉트 기능이 있습니다. 조건에 정규표현식을 쓰거나 User-Agent를 조회하는 것도 가능하죠. 하지만. 여기에는 한 가지 맹점이 있습니다. 단순히 Negative Lookahead로는 문제를 풀 수가 없다는 것입니다.

수많은 js파일들

이런 것들도 서버에서는 ie-update 가 아닌 다른 리소스의 요청으로 간주합니다. 따라서 이런 파일조차 ie-update 로 리다이렉트 해버리므로, .js 파일의 응답이 HTML 페이지가 되어 먹통이 됩니다.

이것을 해결하기 위해서는, 무엇이 페이지고 무엇이 리소스 요청인지를 구분해야 합니다.

페이지 목록을 뽑자

가장 직관적으로 떠오르는 방법은, 모든 페이지의 목록을 만들고 그걸 조회하는 것입니다.

// 매우 간소화된 예시
const nonIePage = ['/', '/bizes', ... ]
if (isIe && nonIePage.some(url => pathname(request.url) === url)) {
res.redirect('/ie-update')
}

하지만 이렇게 하드코딩하는 것이 좋은 방법이 아니라는 것은 자명합니다. 때마침 Next.js의 흥미로운 특징이 눈에 들어왔는데요, 바로 pages 디렉토리 밑에 정형화된 규칙으로 파일을 배치한다는 것입니다.

그렇다면, 만약 디렉토리를 읽어서 자동으로 페이지 목록을 추출한다면? 문제가 쉽게 해결될 것입니다. 실제로 이 작업은 약간의 시행착오를 제외하면 큰 무리 없이 이루어졌습니다.

  • Next.js의 Dynamic Route 경로를 실제 URL과 비교하는 로직 구현 필요
  • 이 로직이 생각보다 복잡하여, Jest를 통한 유닛 테스트 수행
  • 2단계 문제를 고려한다면, Next.js 서빙 미들웨어가 아닌, 커스텀 Express 서버 미들웨어에서 대응
페이지 구조를 탐색하는 간단한 재귀함수 traverse
Coverage가 목적이 되어서는 아니지만, 좋은 코드를 만드는데 도움된다는 점은 부정할 수 없습니다.

이렇게 추출한 변수는 런타임에서 바뀔 일이 없으므로, pagelist.json 파일로 만들어 둡니다. 이 파일은 빌드할 때 마다 생성하게 하여, 신선도에 문제가 없도록 하였습니다.

2. ie-update를 Next.js에서 격리

1번 문제는 상대적으로 쉽게 풀 수 있었고, 곧 정기 배포에도 나갔습니다. 그러나 이대로 끝내기엔 몇 가지 걸리는 점이 있었습니다.

결국 ie-update 페이지를 Next.js에서 서빙한다면, IE 지원을 계속하는 것과 동일하지 않을까?

즉 근본적인 문제는 해결을 못 한 것과 다름 없습니다. 여전히 ES5로 트랜스파일을 해야 했고, 여전히 모든 코드가 IE를 의식해야 하니까요.

따라서 저는 Next.js와는 별개의 서빙 체인을 구성하기로 하였습니다. 이때 저는 한 가지 추가적인 목표를 세웠습니다. 바로, 기존의 개발 경험을 저해하지 않도록 최대한 동일한 개발 환경을 구축하는 것입니다.

격리구역과 Next.js의 공존을 위한 구조

IE 페이지만 React나 PostCSS를 제외할 순 없는 노릇이며, 페이지 상단의 헤더는 기존의 Next.js 관련 코드와 물려있었습니다. 이것을 IE용으로 재구현 하는 것이 합리적일까요? 추후에 헤더의 요소가 바뀌어야 한다면, 비용이 2배가 될 수 있습니다.

또한 i18n을 위해 사내에서 사용하는 툴이 있는데, 이것 역시 공용 컴포넌트와 연관이 있었습니다. IE 문제가 비단 한국에서만 있는 것도 아니고, 추후에 다른 레거시가 생길 때도 대비를 해야 하기 때문이지요.

ie-update 페이지는 정적인 콘텐츠만 존재합니다. 그러므로 이것들을 해결하여 정적인 파일로 빌드한 뒤 1번문제를 풀며 구축한 Express의 미들웨어에서 서빙을 해주면 됩니다.

보일러 플레이트부터 깔아보자

Next.js는 내부에 Webpack 5를 내장하여 자체적으로 빌드 체인을 갖추고 있습니다. 그런 요람에서 벗어나야 하므로, 우선 Webpack 5를 사용하여 React 환경을 구축하기로 했습니다.

IE 대응을 해야 했기 때문에 Vite는 배제하였고, 남은 선택지는 Parcel 아니면 Webpack 정도였습니다. Parcel은 상세 설정이 매우 불편하기 때문에 유연하고 검증된 Webpack을 선택하였습니다.

이때 저는 creat-react-app을 사용하지 않았는데, 다음과 같은 이유 때문입니다.

  • ES5 트랜스파일 때문에 어차피 eject 가 필수적
  • 불필요한 모듈 설치를 지양하기 위함
  • 모듈 버전을 기존 Next.js 구역의 것들과 맞추기 위함

트랜스파일러로는 swc-loader를 선택하였습니다. 비록 ie-update 페이지의 내용물이 그리 많지는 않지만, Next.js 12 업그레이드 시 들어올 SWC와 결을 맞추기 위함이었습니다. 빌드 속도가 빠른 것도 큰 매력이지요.

격리구역에 React — TypeScript — PostCSS 보일러플레이트 구축 성공
격리구역에 React — TypeScript — PostCSS 보일러플레이트 구축 성공

이번엔 Next.js가 발목을 잡았다

Next.js 구역에 있던 ie-update.tsx 를 격리구역으로 옮기자마자, 수많은 에러를 마주했습니다. 당연하게도 기존 코드는 Next.js의 요람 속에서 돌아간다고 가정하고 짰기 때문입니다.

구체적으로는 <Link>useRouter 처럼 Next.js에서 자체 제공하는 기능을 사용한 경우가 문제가 됩니다. 직접 참조하지 않는 파일이라면, 트리쉐이킹을 통해서 어느 정도 해결이 가능합니다. 그러나 공용 컴포넌트나 커스텀 훅, 유틸 함수에 이런 것들이 들어있다면 이야기가 달라집니다.

게다가 Webpack의 트리쉐이킹은 그리 똑똑하지 않습니다. 안타깝게도 기존 코드 간 참조 관계가 복잡하게 얽혀있었고, 어쩔 수 없이 IE 빌드 체인에 딸려 들어가는 상황이 있었습니다.

결정적으로 환경변수가 문제였습니다. Next.js에서는 환경변수를 NEXT_PUBLIC_ 로 구성하면, 브라우저로 전달합니다. 그러나 격리 구역에는 이런 기능이 없으므로 문제가 생긴 것입니다.

Proxying Component

첫 번째 문제는 <Link> 컴포넌트 및 useRouter 를 구현해주는 것으로 해결했습니다. 어차피 ie-update 페이지에서 복잡한 로직을 사용하지 않기 때문에, 자주 쓰는 최소 기능만 하도록 아래와 같이 래핑하였습니다. (useRouter 역시 비슷한 식으로 구현)

이때 기존에 <Link> 를 참조하는 코드를 건드리지 않기 위해, Webpack의 Alias 옵션을 활용했습니다. next/link 를 보면 ./components/next-proxy 로 경로를 변경합니다.

// webpack-common.config.js
resolve: {
modules: ['node_modules'],
alias: {
next: path.resolve(__dirname, './components/next-proxy'),
... 중략
},
extensions: ['.tsx', '.ts', '.jsx', '.js', '.css'],
},

환경변수를 하드코딩한 파일 만들기

Next.js는 서버에서 돌아가는 React 코드와 클라이언트에서 돌아가는 React 코드를 구별하지 않습니다. 일부 기능을 제외하면 .tsx 에 기술한 함수를 공평하게 실행합니다. 때문에 이것은 Next.js 개발자들도 해결해야 할 문제였을 것입니다.

NEXT_PUBLIC으로 시작하는 환경변수는 어떻게든 전달이 됩니다.

Next.js는 클라이언트에서 참조해야 할 환경변수들을 빌드 과정에 같이 내려보냅니다. 소스코드를 직접 확인하진 않아서 정확히 어느 파일에 어떻게 내려보내는지는 알 수 없으나, 그 존재를 빌드된 파일에서 확인할 수는 있습니다.

저도 동일한 접근법을 사용했습니다. 환경변수가 기술된 파일을 Node.js 스크립트로 읽어서, envInject.js 라는 파일을 동적으로 생성합니다. 이것은 빌드 과정에 포함됩니다.

.env 파일을 읽어서 envInject.js 파일로 변환해주는 간단한 스크립트

위의 문제를 해결하고 폴리필까지 완벽하게 잡아주자, 최종적으로 안정된 결과물이 나왔습니다. PR과 QA를 거쳐 배포된 이 격리 구역은 늠름하게 현역으로 활약하고 있습니다.

그 이외에 했던 삽질들

여기서는 결과물만 깔끔하게 이야기했지만, 사실 이 이면에는 수많은 삽질과 시행착오가 숨어있습니다. 그닥 재미있는 이야기는 아니라서 여기서는 명예로운 언급만 하고 넘어갑니다.

  • CSS Module의 확장자(.module.css)만 정상적으로 resolve하지 못하는 문제 → Resolve 옵션에 .css 추가
  • 일반적인 Asset 경로와 CSS import의 경로가 불일치
  • Webpack 트리쉐이킹 적용을 위해, 사이드이펙트가 있는 파일들과 그렇지 않은 99%의 파일들에 서로 다른 Rule 적용
  • 공용 패키지는 사정상 CJS로 빌드. 이로 인해 트리쉐이킹 대상에서 제외되자, 제거되지 못한 미사용 함수의 ES6 문법이 문제를 일으킴. 결국 해당 패키지는 ES5로 트랜스파일하도록 변경.
  • 프로덕션/개발환경 빌드에서 결과물의 위치가 미묘하게 달라, pagelist.json 파일 참조에 문제 발생
  • 기존 yarn script에 자연스럽게 묻혀갈 수 있도록 빌드 체인 만들기

후일담

그로부터 약 한 달이 흘렀습니다. 이 글을 쓰는 지금 돌이켜보면, 굳이 이렇게 까지 해야했을까? 라는 생각도 들긴 합니다. 당시 크게 긴급한 이슈가 없었기 때문에, 한계까지 몰아붙여서 연구를 할 수 있었습니다. 만약 바쁜 시기였다면 2단계를 완벽하게 수행하기는 어려웠을 것이라 생각합니다.

하지만 이 과정에서 얻은 것들은 정말 많았습니다.

  • 여러 도구를 유기적으로 엮기 위해, 정보 수집 능력 향상
  • 평소에 쓸 일 없는 Webpack의 심층 옵션 학습
  • Next.js에 대한 좀 더 깊은 이해
  • 문제의 원인을 파악하는 센스가 조금이나마 생긴 것 같음
  • 재밌음!

한편 IE 이슈를 더이상 고려하지 않아도 되는 Next.js 구역은, 얼마 전 12버전으로 무사히 업그레이드를 마쳤습니다🎉. 연초에 했던 삽질을 생각하면, 허탈할 정도로 쉽게 업그레이드 되었는데요. 6개월동안 Next.js 개발팀도 부단한 노력을 했으리라 생각합니다.

한때 인터넷 시장을 주도했던 IE를 “격리구역”이라는 이름으로 유배보내며 여러가지 생각이 드네요. 모든 것은 언젠가 사라질 수 있고, 그 변화에 유연하게 대처할 수 있는 것만이, 사라지지 않는 방법이 아닐까 생각합니다.

--

--