[TOC]
1. 개요
웹 프로젝트를 진행하다 보면 항상 간과하고 넘어가다 뒤통수 씨게 맞는게 있다.
바로 구버젼 호환성. 그나마 근래에는 Internet-Explorer 지원을 요구하지 않는곳들이 많기도 하고,
webpack, vite 등의 bundler 에서 transpile 해 주는 부분이 있어, 난이도는 많이 낮아지기는 했으나.
정말 의외로 구버젼 브라우저호환이 문제되는 프로젝트가 꽤 있는 편이다.
필자의 경우 어떤 프로젝트 진행 후반에 chrome 60 버젼에서 페이지가 표시되지 않는다고해서 고생한 기억이 있다.
문제는 chrome 60 버젼 출시년도가 2017 년도인데 글 작성일 기준(2024)으로도 채 10년이 되지 않았다는 점이다.
2. 그래서.
글제목을 보고 ie-11 지원방법을 찾아 들어온 사람들은 실망할지 모르겠지만. 일단은
최저 기준은 chrome 60 지원 을 목표로 테스트를 수행했다. (현시점 ie-11 지원은 불가능 까지는 아닐지라도 정말 험난한 고난의 길이다.)
대상은 일단은 아래 주소에서 받은 프로젝트를 기준으로 해 보겠다. [ '스터디레포트' 참고 ]
그리고. 어느정도 javascript 의 역사를 알고있어야 이해가 가능 하며, 아마도 시간과 노력이 정말 많이 들 수도 있다.
(패키지를 하나하나 빼고 더하고 빌드하고 테스트 해 가며 작업해야 한다.)
따라서 되도록이면 초기에 프레임워크를 정할때 타겟 브라우저 최저버젼을 미리 확인하고 프레임워크를 정하길 바란다.
3. 준비
먼저 프로젝트를 내려받고 nextjs-web
폴더에 들어가 패키지를 설치한다 (npm install
)
$ git clone https://gitlab.ntiple.com/developers/study-202403.git
... 중략 ...
$ cd study-202403/nextjs-web
$ npm install
npm WARN deprecated @humanwhocodes/object-schema@2.0.3: Use @eslint/object-schema instead
... 중략 ...
added 498 packages, and audited 499 packages in 23s
173 packages are looking for funding
run `npm fund` for details
1 high severity vulnerability
To address all issues, run:
npm audit fix --force
Run `npm audit` for details.
이후 npm run build
, npm run start
명령으로 프로젝트를 빌드, 구동 시켜본다.
$ npm run build
> my-app@0.1.0 build
> next build
================================================================================
샘플앱 / 프로파일 : local / 구동모드 : build / API프록시 : http://localhost:8080
================================================================================
Attention: Next.js now collects completely anonymous telemetry regarding usage.
This information is used to shape Next.js' roadmap and prioritize features.
You can learn more, including how to opt-out if you'd not like to participate in this anonymous program, by visiting the following URL:
https://nextjs.org/telemetry
▲ Next.js 14.2.7
... 중략 ...
├ ○ /usr/usr01001s02 542 B 473 kB
└ ○ /usr/usr01001s03 1.72 kB 475 kB
+ First Load JS shared by all 474 kB
├ chunks/framework-ecc4130bc7a58a64.js 45.2 kB
├ chunks/main-49d125ccecdddcc4.js 36.7 kB
├ chunks/pages/_app-e56c7d86e4e7c991.js 389 kB
└ other shared chunks (total) 2.87 kB
○ (Static) prerendered as static content
$ npm run start
> my-app@0.1.0 start
> next start
▲ Next.js 14.2.7
- Local: http://localhost:3000
✓ Starting...
================================================================================
샘플앱 / 프로파일 : start / 구동모드 : start / API프록시 : http://localhost:8080
================================================================================
✓ Ready in 913ms
이후 chrom 60
버젼으로 접근해 보면.... ( https://www.slimjet.com/chrome/google-chrome-old-version.php 에서 다운로드 가능 )
아무것도 안나온다........................
.....
콘솔에서 오류라인 클릭해서 찾아가 보면 ( _app-xxxxx.js 클릭 )
라고 뜬다.
원인은 물음표 2개 (double questionmark
) 가 쓰인 문법 에서 걸린것인데 구글에서 용어를 찾아보면..
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Nullish_coalescing
Nullish coalescing operator
라고 뜬다. (MDN 사이트를 찾아보면 chrome 80
이상부터 지원한다고 한다)
같은 방법으로 (과정은 생략) chrome 60
에서 지원하지 않는 문법 연산자 (operation
) 들을 계속 찾아보면
- ※ 프로젝트 내 사용 문법 및 연산자 정리
연산자 형태(문법) 설명 비고 Nullish coalescing operator A ?? B A 가 null 인 경우 B 리턴 chrome 80 부터 Optional catch binding try { } catch { } catch 절에 인자가 필요 없음 chrome 66 부터 Optional chaining B = A?.test() A 가 null 이 아닌경우 test 멤버를 실행하여 B 에 리턴 chrome 80 부터 Logical assignment operators A &&= 2, B ||= 3 A 가 참일경우 2 리턴, B 가 거짓일경우 3 리턴 chrome 85 부터
들이 프로젝트 구성요소(node_modules
) 에 사용된 것을 확인할 수 있다.
그리고 위 문법이 사용된 모듈도 찾아야 하는데, 위 오류에서 Nullish coalescing operator
오류가 발생된 소스를 발췌해서 보면
... let i=r.createContext(),o=()=>r.useContext(i)??!1},9730:function(t,e,n){"use strict";n.d(e,{L7:function(){return c},P$:function(){return d},VO:function(){return o},W8:function(){return u},dt:function(){return h},k9:function(){return l}});var r=n(8920),i=n(1156);let o={xs:0,sm:600,md:900,lg:1200,xl:1536},s={keys:["xs","sm","md","lg","xl"],up:t=>`@media (min-width:${o[t]}px)`},a={containerQueries:t=>({up:e=>{let n="number"==typeof e?e:o[e]||e;return"number"==typeof ...
webpack
으로 묶인 소스이기 때문에 해당 부분이 어떤 모듈에 있는지 찾아야 하는데. createContext
, useContext
, containerQueries
등 소스를 특정할 수 있는 단어들이 나온다.
이를 탐색하기 위해 스크립트를 작성했다. (find.mjs
)
/** find.mjs */
import { readFileSync, readdirSync, statSync, } from 'fs'
import { dirname } from 'path'
import { fileURLToPath } from 'url'
/** 현재 폴더 위치 */
const dir = dirname(fileURLToPath(import.meta.url))
/** 찾을 단어 */
const FIND = (process?.argv?.at && process.argv.at(2)) || ''
/** 폴더 탐색 한계 */
const MAX_DEPTH = 10
/** 찾기 함수 */
const findFiles = (dir, depth = 0) => {
const list = readdirSync(`${dir}`)
for (const file of list) {
const fpath = `${dir}/${file}`
const fstat = statSync(fpath)
const isdir = (fstat.mode == 16877)
if (isdir && depth < MAX_DEPTH) {
if ([ ].indexOf(file) !== -1) { continue }
findFiles(fpath, depth + 1)
} else {
const txt = String(readFileSync(fpath))
if (txt.indexOf(FIND) !== -1) {
console.log('FIND:', fpath)
}
}
}
}
if (FIND) {
/** node_modules 폴더의 내용을 확인한다 */
findFiles(`${dir}/node_modules`)
} else {
console.log('USAGE: node find.mjs {word}')
}
위 소스를 사용해 탐색해 본다.
## createContext 사용모듈 탐색
$ node find.mjs createContext
FIND: study-202403/nextjs-web/node_modules/@emotion/react/dist/emotion-element-5486c51c.browser.esm.js
FIND: study-202403/nextjs-web/node_modules/@emotion/react/dist/emotion-element-60389d2b.edge-light.cjs.js
... 중략 ...
FIND: study-202403/nextjs-web/node_modules/styled-jsx/dist/index/index.js
FIND: study-202403/nextjs-web/node_modules/typescript/lib/lib.dom.d.ts
## useContext 사용모듈 탐색
$ node find.mjs useContext
FIND: study-202403/nextjs-web/node_modules/@emotion/react/dist/emotion-element-5486c51c.browser.esm.js
FIND: study-202403/nextjs-web/node_modules/@emotion/react/dist/emotion-element-60389d2b.edge-light.cjs.js
... 중략 ...
FIND: study-202403/nextjs-web/node_modules/react-redux/dist/react-redux.mjs.map
FIND: study-202403/nextjs-web/node_modules/styled-jsx/dist/index/index.js
## containerQueries 사용모듈 탐색
$ node find.mjs containerQueries
FIND: study-202403/nextjs-web/node_modules/@mui/system/breakpoints/breakpoints.js
FIND: study-202403/nextjs-web/node_modules/@mui/system/cssContainerQueries/cssContainerQueries.d.ts
FIND: study-202403/nextjs-web/node_modules/@mui/system/cssContainerQueries/cssContainerQueries.js
FIND: study-202403/nextjs-web/node_modules/@mui/system/modern/breakpoints/breakpoints.js
FIND: study-202403/nextjs-web/node_modules/@mui/system/modern/cssContainerQueries/cssContainerQueries.js
FIND: study-202403/nextjs-web/node_modules/@mui/system/node/breakpoints/breakpoints.js
FIND: study-202403/nextjs-web/node_modules/@mui/system/node/cssContainerQueries/cssContainerQueries.js
이걸 토대로 위의 방법을 반복하여 (빌드 실행 브라우저 테스트 검색....) 찾은 해결대상 모듈을 추려보면 아래와 같다.
-
※ 프로젝트 내 변경 대상 모듈 목록
모듈 설명 기타 @reduxjs redux 모듈 redux redux 모듈 immer redux 의존 모듈 reselect redux 의존 모듈 redux-thunk redux 의존 모듈 @mui mui 모듈 @floating-ui mui 의존모듈 @popperjs mui 의존모듈 @emotion mui 의존모듈 clsx mui 의존모듈 prop-types mui 의존모듈 react-is mui 의존모듈
크게보면 redux
, mui
와 그 의존모듈에서 발생하고 있는 걸 확인할 수 있다.
다만.... 이런거 다 찾을 때 까지 해결하고 빌드하고 브라우저로 확인해 보고 하는 노가다를 다 될때 까지 무한반복해야 한다....
Good-Luck!!👍
4. 문제되는 소스는 찾았으니 이제 해결해 보자
해결하는 방법은 transpile
이라는 것을 거쳐야 하는데 아직까지 swc 에서는 세세하게 지원하고 있는것 같지않아, babel 에 의존해야 한다.
하지만 next-js
에서는 babel 과 swc 둘 중 하나만을 써야 된다고 하는데...
babel
은 느리다. 그리고 node_modules
에 있는것들이 문제를 일으킨다.
그러면 node_modules
에 있는것들을 babel 로 한번만 transpile 시켜놓으면 다음부터는 swc를 사용할 수 있겠네!!!!
라는 결론에 도달해서 별도로 tools
폴더를 만들고 (본 프로젝트에 영향을 끼치지 않도록) babel과 위에서 발견된 파일들을 transpile 할 플러그인을 설치했다.
$ mkdir tools
$ cd tools
$ echo '{}' > package.json
## babel-cli 및 문법 transpile 용 플러그인
$ npm install @babel/cli @babel/core @babel/plugin-proposal-optional-catch-binding @babel/plugin-transform-logical-assignment-operators @babel/plugin-transform-nullish-coalescing-operator @babel/plugin-transform-optional-chaining
## 나중에 필요해서 추가
$ npm install md5
... 중략 ...
그리고 본격적으로 node_modules
를 탐색하고 찾아서 변조해줄 스크립트를 작성한다. (transpile.mjs
)
/** transpile.mjs */
import { existsSync, statSync, readdirSync, readFileSync, writeFileSync } from 'fs'
import { basename, dirname, parse } from 'path'
import { fileURLToPath } from 'url'
import md5 from 'md5'
import babel from '@babel/core'
/** 프로젝트폴더위치 (현재 실행파일 의 상위폴더) */
const dir = dirname(dirname(fileURLToPath(import.meta.url)))
/** 최대 탐색 깊이 */
const MAX_DEPTH = 10
/** 작업할 파일목록 */
const WORKLIST = []
const findFiles = (dir, depth = 0) => {
const list = readdirSync(`${dir}`)
for (const file of list) {
const fpath = `${dir}/${file}`
const fstat = statSync(fpath)
const isdir = (fstat.mode == 16877)
if (isdir && depth < MAX_DEPTH) {
if ([ ].indexOf(file) !== -1) { continue }
findFiles(fpath, depth + 1)
/** js, cjs, mjs 파일만 작업목록에 입력 */
} else if (/(\.js|\.cjs|\.mjs)$/.test(file)) {
WORKLIST.push(fpath)
}
}
}
/** 소스변환(transpile) */
const transpile = (path) => {
let doTrans = true
try {
const hashpath = `${dirname(path)}/.${basename(path)}.hash`
const src = readFileSync(path)
/**
* 변조작업이 여러번 진행되면 부작용이 발생할수 있으므로 1번만 수행해야 한다.
* 따라서 파일의 해시를 남겨 변조여부를 판단하도록 한다.
**/
if (existsSync(hashpath)) {
const hash1 = md5(src)
const hash2 = String(readFileSync(hashpath))
if (hash1 == hash2) { doTrans = false }
}
if (doTrans) {
console.log('TRANSPILE:', path)
const out = babel.transformSync(src, {
/** chrome 60 에서 작동하지 않는 문법 transpile 용 플러그인들 */
plugins: [
'@babel/plugin-transform-nullish-coalescing-operator',
'@babel/plugin-proposal-optional-catch-binding',
'@babel/plugin-transform-optional-chaining',
'@babel/plugin-transform-logical-assignment-operators',
]
})
writeFileSync(path, out.code)
writeFileSync(hashpath, md5(out.code))
}
} catch (e) {
console.log('CANNOT CONVERT: ', path)
}
}
/** 작업할 모듈들 (폴더목록) */
[
`${dir}/node_modules/@reduxjs`,
`${dir}/node_modules/redux`,
`${dir}/node_modules/immer`,
`${dir}/node_modules/reselect`,
`${dir}/node_modules/redux-thunk`,
`${dir}/node_modules/react-redux`,
`${dir}/node_modules/@mui`,
`${dir}/node_modules/@floating-ui`,
`${dir}/node_modules/@popperjs`,
`${dir}/node_modules/@emotion`,
`${dir}/node_modules/clsx`,
`${dir}/node_modules/prop-types`,
`${dir}/node_modules/react-is`,
].map(v => findFiles(v))
/** 폴더에서 발견된 작업대상 파일들을 변조해 준다. */
WORKLIST.map(v => transpile(v))
transpile.mjs
파일을 작성했다면 아래와 같이 (node transpile.mjs
) 실행해 준다.
$ node transpile.mjs
... 중략 ...
TRANSPILE: /home/coder/documents/study-202403/nextjs-web/node_modules/react-is/cjs/react-is.development.js
TRANSPILE: /home/coder/documents/study-202403/nextjs-web/node_modules/react-is/cjs/react-is.production.min.js
TRANSPILE: /home/coder/documents/study-202403/nextjs-web/node_modules/react-is/index.js
TRANSPILE: /home/coder/documents/study-202403/nextjs-web/node_modules/react-is/umd/react-is.development.js
TRANSPILE: /home/coder/documents/study-202403/nextjs-web/node_modules/react-is/umd/react-is.production.min.js
꽤 많은 파일들을 탐색하므로 시간이 걸리는 편이다.
몇 몇 파일들은 transpile 과정에서 실패하게 되지만 대다수의 파일들이 transpile 되어졌다.
다시 프로젝트 폴더로 돌아와서 빌드, 구동하고
$ cd ..
$ npm run build
$ npm run start
이제 브라우저로 확인해 보자......
5. 하지만 여기서 끝이 아니다!!!
뭐.. 뭐요? 뭐가 없다구요?
오류가 발생했다. ReferenceError: queueMicrotask is not defined
queueMicrotask 라는 객체가 없단다.
구형브라우저 서포트 (legacy-support) 는 2가지 측면이 모두 이루어 져야 한다.
지금껏 해 준 과정은 신규문법 미지원 문제를 소스코드 변조(transpile, 일종의 통역)로 해결해 준 과정이다.
또다른 측면은 신규객체(및 함수)가 없는 문제를 해결해 주는 과정이 필요하다. 보통은 이를 구멍난 곳을 메꾸어준다 라는 개념으로 polyfill 이라고 한다.
먼저 core-js
라는 패키지를 설치해 준다. (polyfill 의 가장 대표적인 패키지이며 왠만한 모듈은 거의 다 포함하고 있다.)
npm install -D core-js
_app.jsx
(또는 entry-point) 파일의 import 구문 최상단에 다음과 같이 core-js
를 import 해준다 (반드시 최상단 import 이어야 한다.)
import 'core-js'
import Head from 'next/head'
import { AnimatePresence } from 'framer-motion'
import LayoutDefault from '@/components/layout'
... 중략 ...
보통은 여기까지만 해 주어도 대부분 해결 되지만(발견 과정은 똑같으니 생략하고), 또다시 오류 발생후 먹통이다.............
........
괜찮아! 이젠 익숙해질때도 됐잖아👍
6. 드디어 최종해결
같은 작업의 연속이니 요점만 짚고 빠르게 넘어 가겠다.
이번에는 AbortControl 이 없어서발생한 문제이고 (ReferenceError: AbortSignal is not defined
) api.ts
에서 사용중이다 (timeout 관련).
abortcontroller-polyfill
모듈을 설치해 준다.
npm install -D abortcontroller-polyfill
AbortController
를 사용하도록 관련 코드를 수정해 준다.
/** 구형브라우저 지원용 polyfill */
import 'abortcontroller-polyfill'
import * as C from '@/libs/constants'
import app from './app-context'
import userContext from './user-context'
import crypto from './crypto'
type OptType = {
method?: String
apicd?: String
resolve?: Function
reject?: Function
/** timeout abort 취소용메소드 */
abortclr?: Function
}
const { putAll, getConfig, log } = app
const keepalive = true
/** 초기화, 기본적으로 사용되는 통신헤더 등을 만들어 준다 */
const init = async (method: string, apicd: string, data?: any, opt?: any) => {
const headers = putAll({}, opt?.headers || {})
const timeout = opt?.timeout || getConfig()?.api?.timeout || 10000
/** timeout 구현을 위해 AbortController 생성 (구형 브라우저 지원용) */
const abortctl = new AbortController()
const signal = abortctl.signal
... 중략 ...
/** timeout 시간동안 request 가 처리되지 않으면 abort signal 발생 */
const hndtimeout = setTimeout(abortctl.abort, timeout)
/** 정상처리되어 abort signal 이 발생하지 않도록 clear 한다. */
const abortclr = () => clearTimeout(hndtimeout)
return { method, url, body, headers, signal, abortclr }
}
/** 통신결과 처리 */
const mkres = async (r: Promise<Response>, opt?: OptType) => {
let ret = { }
let t: any = ''
const resp = await r
const hdrs = resp?.headers || { get: (v: any) => {} }
/** 정상처리 되었으므로 abort signal 취소 */
opt?.abortclr && opt.abortclr()
... 중략 ...
}
const api = {
nextping: 0,
/** PING, 백엔드가 정상인지 체크하는 용도 */
async ping(opt?: any) {
return new Promise<any>(async (resolve, reject) => {
const apicd = `cmn00000`
const curtime = new Date().getTime()
if (opt?.noping) { return resolve(true) }
if (curtime < api.nextping) { return resolve(true) }
const { method, headers, signal, url, abortclr } = await init(C.GET, apicd, opt)
const r = fetch(url, { method, headers, signal, keepalive })
const res: any = await mkres(r, putAll(opt || {}, { apicd, method, resolve, reject, abortclr }))
/** 다음 ping 은 10초 이후 */
api.nextping = curtime + (1000 * 10)
return res
})
},
/** POST 메소드 처리 */
async post(apicd: string, data?: any, opt?: any) {
return new Promise<any>(async (resolve, reject) => {
await api.ping(opt)
const { method, url, body, headers, signal, abortclr } = await init(C.POST, apicd, data, opt)
const r = fetch(url, { method, body, headers, signal, keepalive })
return await mkres(r, putAll(opt || {}, { apicd, method, resolve, reject, abortclr }))
})
},
/** GET 메소드 처리 */
async get(apicd: string, data?: any, opt?: any) {
return new Promise<any>(async (resolve, reject) => {
await api.ping(opt)
const { method, url, headers, signal, abortclr } = await init(C.GET, apicd, data, opt)
const r = fetch(url, { method, headers, signal, keepalive })
return await mkres(r, putAll(opt || {}, { apicd, method, resolve, reject, abortclr }))
})
},
/** PUT 메소드 처리 */
async put(apicd: string, data?: any, opt?: any) {
return new Promise<any>(async (resolve, reject) => {
await api.ping(opt)
const { method, url, body, headers, signal, abortclr } = await init(C.PUT, apicd, data, opt)
const r = fetch(url, { method, body, headers, signal, keepalive })
return await mkres(r, putAll(opt || {}, { apicd, method, resolve, reject, abortclr }))
})
},
/** DELETE 메소드 처리 */
async delete(apicd: string, data?: any, opt?: any) {
return new Promise<any>(async (resolve, reject) => {
await api.ping(opt)
const { method, headers, signal, url, abortclr } = await init(C.DELETE, apicd, data, opt)
const r = fetch(url, { method, headers, signal, keepalive })
return await mkres(r, putAll(opt || {}, { apicd, method, resolve, reject, abortclr }))
})
},
/** URL 을 형태에 맞게 조립해 준다 */
mkuri(apicd: string) {
const mat: any = apicd && /^([a-z]+)([0-9a-zA-Z]+)([/].*){0,1}$/g.exec(apicd) || {}
if (mat && mat[1]) {
return `${app.getConfig()?.api?.base || '/api'}/${mat[1]}/${mat[0]}`
} else {
return apicd
}
}
}
export default api
이제 빌드하고 브라우저에 띄어보면
잘된다!
끄~~~읕!!!!
이번 포스팅에서 작성하거나 변경한 주요 파일 목록은 아래와 같다.
├── src
│ ├── libs
│ │ └── api.ts (abortcontroller polyfillbort)
│ └── pages
│ └── _app.jsx (core-js polyfill)
├── tools
│ ├── package.json (babel 설치후 생성파일)
│ └── transpile.mjs (node-modules 소스 변조)
├── find.mjs (node-modules 소스 탐색)
└── package.json (core-js, abortcontroller 설치후 변경파일)
내용은 아래 주소에서 다운로드 가능하다.
7. 결론
이번 포스팅에서는 구형브라우저 지원 (legacy support) 에 대해 다루어 보았다.
원래대로라면 vanilla-javascript
, cjs
, mjs
를 거쳐 ecma 표준과 typescript 까지 역사를 알고 있어야 하고
그 외 system-js
, umd
까지 의 지식도 일부 필요로 한다.
즉 개발 이외의 알아야 할 것들이 너무나도 많아진다
이거 다 설명하려면 지루하기도 지루하거니와 심지어 필자도 잘 모르는 부분이 많기 때문에 되도록 간략하게 설명 하려고 노력했다.
그리고 이번편 처럼 단순히 코딩 빌드 확인 재코딩 의 무한 반복이 계속되는 경우도 흔하다.
보통은 프로그래밍 입문 하면서 인텔리하고 멋있어 보인다 또는 재미있어보인다 라는 생각으로 입문들 하게되는데
(요새 세대는 다르려나? 적어도 라떼는 그랬다.)
실상을 까보면 이렇게 단순로동의 연속일 때가 많다.
그래도 알고 맞으면 좀 덜 아프다고.
개발자들이 이런경우에 대한 대비책에 조금이라도 도움이 되었으면 한다.