본문으로 건너뛰기

"javascript" 태그로 연결된 112개 게시물개의 게시물이 있습니다.

모든 태그 보기

rollup-plugin-postcss 의 path alias 문제

· 약 5분

scss alias

// ~는 노드 모듈 처리된다.
@import "~@material";

// @styles 로 사용하고 싶을 경우가 있다.
@import "@styles/mystyle";
  • 두 번째의 경우는 scss 시트를 처리해주면서 (plugin postcss) Error: Can't find stylesheet to import. 와 같은 에러를 뱉는다.
  • 웹팩의 경우 resolve.alias 가 알아서 처리를 해주지만, rollup-plugin-postcss 의 sass-loader 에서 importer 를 추가해줘야할 것 같았다.

sass-loader

  • 이 플러그인의 sass-loader 는 dart-sass 의 importer 를 확장해서 쉽게 추가가 가능해보였다.
  • 그러나 아무리 확장처리를 해도 importer 에서 해당 경로가 리졸브 되지 않았고 관련 이슈가 3개, PR이 2개 등록된 것을 확인할 수 있다.
  • 이 걸 해결하려면 postcss.use.sass 사용시에 importer 를 override 해주는 custom sass-loader 를 직접 만들어야한다.

custom-sass-loader

  • rollup-plugin-postcss/sass-loader.js
  • pity 는 promisify 로 대체 가능하여 지웠고 import-cwd 도 node 코어를 사용하게 변경했다.
  • p-queue 도 child_process 로 대체 가능해보인다.
import { createRequire } from "module";
import path from "path";
import { promisify } from "util";

import resolve from "resolve";
import PQueue from "p-queue";

function loadModule(moduleId) {
try {
return require(moduleId);
} catch {
// Ignore error
}

try {
return createRequire(path.resolve(process.cwd(), "noop.js"))(moduleId);
} catch {
// Ignore error
}
}

// This queue makes sure node-sass leaves one thread available for executing fs tasks
// See: https://github.com/sass/node-sass/issues/857
const threadPoolSize = process.env.UV_THREADPOOL_SIZE || 4;
const workQueue = new PQueue({ concurrency: threadPoolSize - 1 });

const moduleRe = /^~([a-z\d]|@).+/i;

const getUrlOfPartial = (url) => {
const parsedUrl = path.parse(url);
return `${parsedUrl.dir}${path.sep}_${parsedUrl.base}`;
};

const resolvePromise = promisify(resolve);

// List of supported SASS modules in the order of preference
const sassModuleIds = ["node-sass", "sass"];

export default {
name: "sass",
test: /\.(sass|scss)$/,
process({ code }) {
return new Promise((resolve, reject) => {
const sass = loadSassOrThrow();
const render = promisify(sass.render.bind(sass));
const data = this.options.data || "";
workQueue.add(() =>
render({
...this.options,
file: this.id,
data: data + code,
indentedSyntax: /\.sass$/.test(this.id),
sourceMap: this.sourceMap,
importer: [
(url, importer, done) => {
if (!moduleRe.test(url)) {
// 이부분에 alias importer 가 추가되어야한다.
if (/^@styles/.test(url)) {
return done({
file: url.replace(
/^@styles/,
path.resolve(__dirname, "./src/styles"),
),
});
}
return done({ file: url });
}
const moduleUrl = url.slice(1);
const partialUrl = getUrlOfPartial(moduleUrl);

const options = {
basedir: path.dirname(importer),
extensions: [".scss", ".sass", ".css"],
};
const finishImport = (id) => {
done({
// Do not add `.css` extension in order to inline the file
file: id.endsWith(".css") ? id.replace(/\.css$/, "") : id,
});
};

const next = () => {
// Catch all resolving errors, return the original file and pass responsibility back to other custom importers
done({ file: url });
};

// Give precedence to importing a partial
resolvePromise(partialUrl, options)
.then(finishImport)
.catch((error) => {
if (
error.code === "MODULE_NOT_FOUND" ||
error.code === "ENOENT"
) {
resolvePromise(moduleUrl, options)
.then(finishImport)
.catch(next);
} else {
next();
}
});
},
].concat(this.options.importer || []),
})
.then((result) => {
for (const file of result.stats.includedFiles) {
this.dependencies.add(file);
}

resolve({
code: result.css.toString(),
map: result.map && result.map.toString(),
});
})
.catch(reject),
);
});
},
};

// 이하 생략...

rollup.config.js

위의 커스터마이징된 sass-loader 를 use: ['sass'] 대신 등록해준다.

postcss({
// 추가
loaders: [customSassLoader],
// 제거
// use: ['sass']
});

결론

  • rollup-plugin-postcss 는 이슈가 있어 확장을 직접해야하고 이는 유지보수 포인트로 다가올 수 있다.
  • postcss-import 는 alias 기능이 없어 사용이 불가능하다.
  • postcss-import-alias 등등의 alias 를 추가한 라이브러리도 불가능한데, 이는 postcssLoader 가 먼저 등록 되는데 의외의 결과이다. rollup-plugin-postcss 를 직접 로컬에서 빌드해서 해당 지점을 확인해볼 수 있겠지만 PR 을 받지 않으므로 의미도 없다.
  • rollup-plugin-scss를 사용하고 postcss 를 processor 로 줘서 반대로 처리할 수 있어보이는데, 빌드 파이프라인을 다 리팩토링을 해야하므로 나중에 도전해보는걸로 하자.

ecma 스펙으로 알아보는 prototype

· 약 5분

개요

  • prototype 은 constructor 와 상속을 이해하기 위해 필요하다.
  • 여러 코드 예시를 가지고 이걸 설명하려고 하는 포스팅이 많은데 이것도 정작 중요한 스펙에 대한 내용이 다 빠져있다.

Prototype

다른 객체에 공유 프로퍼티를 제공하는 객체

  • NOTE: 생성자가 객체를 만들 때 그 객체는 암묵적으로 생성자의 prototype 속성을 참조한다.
  • 생성자의 prototype 프로퍼티는 constructor.prototype 에 의해 참조될 수 있고 객체의 프로토타입에 추가된 프로퍼티는 상속을 통해 프로토타입을 공유하는 모든 객체에 공유된다.
  • Object.create 함수를 사용하여 명시적으로 지정된 프로토타입으로 객체를 만들 수 있다.

new 실행 단계

ECMAScript의 function 객체 F에 대한 내부 메소드 [[Construct]] 가 argumentsList 와 newTarget 파라미터와 함께 호출된다. argumentsList 는 빈 List 가 가능하다.

  1. Assert: F는 ECMAScript function object이다.
  2. Assert: newTarget은 Object이다.
  3. Let callerContext를 현재 execution context로 설정한다.
  4. kind는 F.[[ConstructorKind]]로 설정한다.
  5. if kind가 base라면
    • a. thisArgument를 newTarget의 prototype을 가지는 새로운 객체로 설정한다.
  6. calleeContext는 PrepareForOrdinaryCall(F, newTarget)로 설정한다.
  7. Assert: calleeContext는 이제 현재 실행 중인 execution context이다.
  8. If kind가 base라면, OrdinaryCallBindThis(F, calleeContext, thisArgument)로 this 바인딩을 한다.
  9. constructorEnv를 calleeContext의 LexicalEnvironment로 설정한다.
  10. envRec를 constructorEnv의 EnvironmentRecord로 설정한다.
  11. 함수 F를 argumentList로 실행한 결과를 result로 설정한다.
  12. calleeContext를 execution context stack에서 삭제하고 callerContext를 현재 execution context로 복구한다.
  13. If result.[[Type]]이 return이면
    • a. result.[[Value]]가 객체이면 result.[[Value]]를 반환한다.
    • b. If kind가 base이면 thisArgument를 반환한다.
    • c. If result.[[Value]] 가 undefined 가 아니라면 TypeError를 발생시킨다.
  14. 아니라면, result를 반환한다.
  15. EnvironmentRecord의 this를 반환한다.

5.a

  • 여기서 this를 newTarget의 prototype을 가지는 객체로 생성한다.
  • 이 사양으로 프로토타입 체인이 실행된다.

8

  • 위에서 생성된 객체를 this로 설정한다.

11

  • argumentsList를 파라미터로 넘긴 생성자 함수 F로 새로운 객체를 초기화한다.

15

  • 초기화된 this를 함수 호출의 결과로 반환한다.
  • 이 사양으로 new 키워드로 실행시 return this; 구문이 없는데도 인스턴스가 반환된다.

기타 특성

  • 함수 객체의 prototype은 Object.prototype에 대한 delegation link와 constructor 속성을 가진 객체의 참조를 가진다.
  • constructor 속성은 함수 객체에 대한 역참조를 가지고 있다. (재귀적)
  • 함수는 Function.prototype 에 대한 델리게이션 링크를 가지고 있어 apply와 call을 상속받는다.

__proto__ 와 prototype 의 차이

  • __proto__ 는 내부 슬롯인 [[Prototype]]에 접근하는 접근자 프로퍼티다.
  • 함수 객체만이 가지고 있는 prototype 프로퍼티는 생성자 함수가 생성할 인스턴스의 프로토타입을 가리킨다.
    • 따라서 화살표 함수나 메소드 축약 문법으로 생성한 함수는 프로토타입을 가지지 않는다.

참조

ecma 스펙으로 알아보는 this

· 약 3분

개요

  • this 는 함수나 스코프 기반으로 결정되는 것이 아니라 호출 방법에 따라 변경된다.
  • 여러 코드 예시를 가지고 이걸 설명하려고 하는 포스팅이 많은데 이것도 정작 중요한 스펙에 대한 내용이 다 빠져있다.
  • this 가 어떻게 동작하나요? 에 대한 대답은 하나다. ECMA OrdinaryCallBindThis 사양대로 동작합니다.

OrdinaryCallBindThis

OrdinaryCallBindThis가 호출되었을 때 함수 F와 실행 컨텍스트 calleeContext 그리고 ECMAScript 값인 thisArgument 를 활용하여 다음 단계를 수행한다.

  1. thisMode 를 F.[[thisMode]] 으로 설정한다.
  2. if thisMode가 lexical 이면 undefined 를 반환한다.
  3. calleeRealm을 F.[[Realm]] 으로 설정한다.
  4. localEnv를 calleeContenxt 의 LexicalEnvironment 로 설정한다.
  5. if thisMode 가 "strict" 라면 thisValue 는 thisArgument 로 설정한다.
  6. else
    • a. if thisArgument 가 undefined 또는 null 이면
      • i. globalEnv를 calleeRealm.[[GlobalEnv]] 로 설정한다.
      • ii. globalEnvRec는 globalEnv 의 EnvironmentRecord 로 설정한다.
      • iii. Assert: globalEnvRec 는 global EnvironmentRecord 이다.
      • iv. thisValue 를 GlobalEnvRec.[[GlobalThisValue]] 로 설정한다.
    • b. else
      • i. thisValue 를 thisArgument 를 객체로 변환 (ToObject) 하여 설정한다.
      • ii. NOTE: ToObject는 calleeRealm을 사용하여 래퍼 객체를 생성한다.
  7. envRec 를 localEnv 의 EnviromentRecord 로 설정한다.
  8. Assert: envRec 는 function EnviromentRecord 이다.
  9. Assert: envRec.[[ThisBindingStatus]]가 초기화되지 않았으므로 10번은 abrupt completion 을 반환하지 않는다.
  10. envRec.BindThisValue(thisValue) 를 반환한다.
  • 볼드체로 표시한 부분을 잘 보자.

5

  • 이 사양으로 인해 use strict 사용시에 동작이 변경된다.

6.a.iv

  • 이 사양으로 인해 thiswindow로 선언된다.

6.b.i

  • 이 사양으로 인해 this에 값을 넘길 경우 객체로 변경된다.

참조

URL을 입력하면 발생하는 일

· 약 7분

개요

  • 한동안 이 타이틀의 문서가 유행했다.
  • 프론트엔드를 알고있는지에 대해 지표로 확인되는 것 같고, 네트워크와 브라우저의 렌더링 기법까지 알아야하기 때문에 확산되는 듯한데 정작 가장 중요한 캐시레이어에 대한 정보가 없었다.

플로우

  • 모바일 크롬에서 URL을 입력했다고 가정하자.

예외단계

Wifi

  • AP 와 802.11 프로토콜로 연결을 마치면 DHCP 프로토콜을 통해 IP를 할당받는다.

모바일 네트워크

  • 핸드폰이 RRC 유휴상태에 있으므로 폰의 무선 전파가 근처의 중계탑과 동기화를 마친 다음 요청을 보내 무선 전파 컨텍스트를 성립시킨다.
  • 핸드폰이 중계탑에서 리소스를 할당받고 정해진 데이터율과 전력으로 데이터를 전송할 준비를 마친다.
  • 요청시 패킷
    • 코어 네트워크 -> 서빙게이트웨이 -> 패킷게이트웨이 -> 외부 네트워크로 연동된다.
  • 응답시 패킷
    • 외부 네트워크 -> 패킷게이트웨이 -> 서빙게이트웨이 -> 이동성관리엔티티 (MME) -> 핸드폰이 유휴상태일 경우 중계탑에 브로드캐스팅 -> 무선 전파 컨텍스트 재수립 -> 사용자 위치를 서빙게이트웨이로 전송 -> 중게탑과 서빙게이트웨이 간에 터널링 -> 핸드폰으로 응답 전송
  • IP 는 패킷게이트웨이에서 관리한다.
  • RRC 협상 -> DNS 룩업 -> TCP 핸드쉐이크 -> TLS 핸드쉐이크 -> HTTP 요청 순이다.

오프라인

  • 캐시스토리지 내에 캐싱된 페이지 또는 Fallback이 있는지 확인하고 반환한다.
  • 메모리 캐시에 있는지 확인한다.
  • ETag, Must-Revalidate 캐시 컨트롤 헤더를 가지고 있지 않다면 HTTP 브라우저 캐시에 있는지 확인하고 반환한다.

크로미움

  • 크로미움 기반 브라우저는 NXDomain 하이재킹을 방지하기 위해 굉장히 재미있는 일을 한다.
  • 이 코드는 fake 도메인으로 질의를 3회 진행하는 코드이다.

캐싱

  • 메모리 캐시에 있는지 확인하고 반환한다.
  • 캐시스토리지에 있는지 확인하고 반환한다.
  • HTTP 브라우저 캐시에 있는지 확인하고 검증 단계를 거치고 반환한다.
  • HTTP/2 푸쉬 캐시에 있는지 확인하고 반환한다

요청

  • 쿠키가 있으면 요청과 함께 전송할지 결정한다.
  • 요청에 재사용 가능한 커넥션이 있는지 확인한다.
  • 새 커넥션을 열 수 있는지 확인한다. (HTTP/1.1 의 도메인별 커넥션 상한에 제한되는지 확인)
  • 서버의 IP를 알고 있는지 확인한다.
    • 모르는 경우 브라우저 내부 캐시 -> OS 캐시 -> OS 호스트파일을 확인하여 없는 경우 DNS 질의를 시작한다.
    • DNS 질의는 네트워크 설정에서 설정된 IP 또는 DHCP 로 지정된 IP 를 확인한다.
    • ARP 캐싱이 없는 경우 ARP 요청 프레임을 브로드캐스팅하고 자신의 IP 주소가 요청 프레임과 일치하는 단 하나의 기기는 ARP 응답을 보내면서 캐싱한다.
    • IP 데이터그램을 캡슐화하여 네임서버에 질의를 한다.
    • DNS 질의는 기본 UDP, 512Bytes 이상일 경우 TCP 를 이용하며 rfc7766 에 정의되어있다.
    • 네임서버 -> DNS 리커서 (리졸버) 서버 -> 루트 네임서버 -> 최상위 도메인(TLD) 을 통해 IP 로 반환된다.
  • IP 주소를 알았으므로 ARP 로 주소를 확인한다.
  • 스위치 -> 라우터 -> 방화벽을 거친다.
  • 소켓을 열고 핸드쉐이크를 시작한다. (1RTT)
  • 대부분의 사이트가 HTTPS 로 서빙되므로 첫 연결에서는 TLS 핸드쉐이크를 시작한다. (1~2RTT)
    • TLS 버전, 알고리즘, 압축 방식 협상
    • 공개 인증서 반환
    • 암호화

응답

  • 요청된 주소가 엣지서버이면 캐시 만료여부를 확인한 뒤 응답을 반환한다.
  • 요청된 주소에 대해 서버가 응답한다.
  • 응답 스트림의 처음 몇 바이트를 확인하여 악성페이지일 경우 경고 페이지를 표시하고 종료한다.
  • 렌더러 프로세스가 다룰 수 있는 응답일 경우 렌더링을 시작한다.

렌더링

  • HTML, CSS를 파싱한다.
    • 하위 리소스를 로드한다.
    • 파싱 중 script 태그를 만날 경우 로드하고, 파싱하고, 실행하여 HTML 파싱을 일시적으로 차단한다.
  • defer JS를 파싱한다.
  • 렌더 트리를 생성한다.
  • 레이아웃을 계산한다.
  • 레이아웃 트리를 순회하며 페인트 레코드 (순서)를 생성한다.
  • 컴포지터 스레드에서 각 레이어를 레스터라이즈한다.
  • 픽셀을 렌더링한다.

참조

이미지 리사이즈

· 약 2분

CSS로 리사이즈

<!doctype html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Answer</title>
<style>
.resizable {
resize: both;
border: 2px solid blue;
overflow: hidden;
position: relative;
}

.resizable::after {
position: absolute;
display: block;
bottom: 0;
right: 0;
width: 10px;
height: 10px;
background-color: blue;
content: "";
cursor: nwse-resize;
}
</style>
</head>
<body>
<h1>Answer</h1>
<form name="image">
<input type="text" name="src" value="./example.jpg" />
<button type="submit">불러오기</button>
</form>
<script>
const MAX_WIDTH = 800;
const MIN_WIDTH = 300;

const generateRandomId = () => btoa(Math.random()).substr(0, 12);
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
const wrap = mutation.target;
switch (mutation.attributeName) {
case "style": {
let nextWidth = parseInt(wrap.style.width);
if (nextWidth > MAX_WIDTH) {
nextWidth = MAX_WIDTH;
} else if (nextWidth < MIN_WIDTH) {
nextWidth = MIN_WIDTH;
}

wrap.style.width = `${nextWidth}px`;
wrap.style.height = `${nextWidth / wrap.dataset.ratio}px`;
}
}
});
});

const loadImage = (src) => {
const id = generateRandomId();
const img = new Image();
img.src = `${src}?v=${id}`;
img.id = id;
img.onload = (event) => {
const _img = document.getElementById(id);
const _wrap = _img.parentElement;
const { width, height } = _img.getBoundingClientRect();

_wrap.style.width = `${width}px`;
_wrap.style.height = `${height}px`;
_wrap.dataset.ratio = String((width / height).toFixed(2));
_img.style.width = "100%";
_img.style.height = "100%";
};

const wrap = document.createElement("div");
wrap.classList.add("resizable");
wrap.appendChild(img);
document.body.appendChild(wrap);
observer.observe(wrap, {
attributes: true,
});
};

document.forms.image.addEventListener("submit", (event) => {
event.preventDefault();

const src = event.target.src.value;
if (src) {
loadImage(src);
}
});
</script>
</body>
</html>

JS로 리사이즈

  • 시간될 때 구현해보자.
  • handler 를 만들고, nw, ne, sw, se 방향에 childNode를 각각 만들고 after content 로 영역을 생성해야한다.

여담

  • 컴포넌트의 기본 단위라고 생각되는 것들에 대해 vanilla 로 스니펫을 많이 만들어봐야될 것 같다.
  • react-movable React 와 여러 Util Class 를 사용하여 Commits on Aug 12, 2019 부터 만든 것 같은데, 90m 안에 가능했던걸까.

Webpack5 설정

· 약 31분

개요

  • no config 가 유행이지만 적용할 체계에 맞게 튜닝하려면 모든 옵션을 꿰고 있어야할 것이다.

전체 설정

/* eslint-disable no-dupe-keys */
const path = require("path");

module.exports = {
// 모드 설정
// production 일 경우 실환경용 플러그인을 활성화한다.
// https://webpack.js.org/configuration/mode/#mode-production
mode: "production", // "production" | "development" | "none"

// 어플리케이션 구조에 따라 웹팩이 빌드를 시작할 진입점 설정
entry: "./app/entry", // string | object | array

// 웹팩으로 빌드될 파일에 대한 출력 설정
output: {
// 경로 설정 (절대경로)
path: path.resolve(__dirname, "dist"), // string

// 파일명 설정
// https://webpack.js.org/configuration/output/#template-strings
// [id] [name] [fullhash] [contenthash] 등을 사용하여 구성 가능
filename: "[name].js", // string

// public 경로 설정
// 앱 내부에서 사용하는 asset 에 대해 기본 경로 지정 (이미지 등 public dir)
publicPath: "auto", // string

// 웹팩으로 라이브러리를 만드는 경우 사용하는 설정
// 기본값은 undefined
// https://github.com/webpack/webpack/tree/master/examples/multi-part-library
library: {
// 라이브러리 타입 정의
type: "var", // "umd2" | "commonjs-module" | "commonjs2" | "commonjs" | "amd" | "amd-require" | "system" | "this" | "var" | "assign" | "global" | "window" | "self" | "jsonp" | "module"

// 노출할 라이브러리의 이름 설정
name: undefined, // string | string[]

/* 라이브러리 고급설정 */
// 노출 되어야하는 엔트리 모듈 설정
export: undefined, // string | string[]

// UMD 래퍼에 추가할 코멘트
auxiliaryComment: "comment", // { amd: "comment", commonjs: "comment", commonjs2: "comment", root: "comment" },

// umd build 에서 amd define 함수에 이름 설정
umdNamedDefine: undefined,
},

// 빌드의 고유값 설정
// 이는 같은 HTML 에 대해 충돌을 방지한다.
uniqueName: "기본값은 package.json 파일의 name 속성",

// 여러 웹팩 설정을 사용 시에 확인할 이름 설정
name: undefined, // string

/* 고급 출력 설정 */
// 청크파일에 대한 파일명 설정
// long term cache 시에는 [contenthash].js
chunkFilename: "[id].js", // string | (pathData, assetInfo) => string

// 에셋 모듈에 대한 파일명 설정
assetModuleFilename: "[hash][ext][query]", // string

// 웹 어셈블리 모듈에 대한 파일명 설정
webassemblyModuleFilename: "[hash].module.wasm", // string

// 소스 맵 파일명 설정
// devtool: source-map 인 경우만 동작
sourceMapFilename: "[file].map[query]", // "sourcemaps/[file].map"

// 웹팩 devtool 에 대한 템플릿 설정
devtoolModuleFilenameTemplate:
"webpack://[namespace]/[resource-path]?[loaders]", // string | (info) => string

// 웹팩 devtool 에 대한 템플릿 지정 (충돌 방지용)
devtoolFallbackModuleFilenameTemplate: undefined, // string | (info) => string

// 웹 에서 JSONP 로 청크를 로드하는 경우 CORS 설정
crossOriginLoading: false, // "use-credentials" | "anonymous" | false

// import 함수명 (polyfill 사용시 변경)
// dynamic-import-polyfill 의 경우 __import__
importFunctionName: "import", // string

// import meta 명 (polyfill 사용시 변경)
importMetaName: "import.meta", // string

/* 전문가용 출력 설정 1 (위험) */
// 번들에 pathinfo 정보 추가 (production 에서 비활성화)
pathinfo: true, // boolean

// script tag 에 charset=utf-8 속성 추가
// 모던 브라우저에서 deprecated 되었지만 호환성을 위해 웹팩에서 기본으로 추가
charset: true, // string

// chunk 파일 타임아웃 설정
chunkLoadTimeout: 120000, // number (ms)

// 생성된 애셋을 디스크에 쓰기 전에 비교하여 일치할 경우 덮어쓰지 않음
compareBeforeEmit: true, // boolean

// require 시에 에러 발생을 추적할지 설정 (퍼포먼스 이슈로 비활성화가 기본값)
strictModuleExceptionHandling: false, // boolean

// devtools 의 소스 네임스페이스
devtoolNamespace: output.uniqueName, // string

// 출력 환경 설정
environment: {
// 화살표 함수 지원
arrowFunction: true,
// 123n 과 같은 bigInt 지원
bigIntLiteral: false,
// const 지원
const: true,
// destructing 연산자 지원
destructuring: true,
// import() 지원
dynamicImport: false,
// for of 문 지원
forOf: true,
// import / export 지원
module: false,
},

// umd 와 같은 라이브러리의 경우 어느 전역 개체에 마운트할 지 설정
globalObject: "self", // string,

// 번들을 IIFE 로 감싸 isolation 을 줄지 설정
iife: true, // boolean

// 모듈 유형의 자바스크립트 파일로 생성할지 설정
// experiments.outputModule: true 로 실험 기능을 켜야하며 사용시 iife: false 로 설정된다.
module: false, // boolean

// 스크립트 타입 설정
// output.module 이 true 일 경우 이 값도 module 로 설정됨
scriptType: false, // boolean | "module" | "text/javascript"

/* 전문가용 출력 설정 2 (위험) */
// 청크 파일을 로드 방법 설정
// web: jsonp, worker: importScritps, sync node.js: require, async node.js: async-node
chunkLoading: "jsonp", // "jsonp" | "import-scripts" | "require" | "async-node" | false

// 청크 파일을 등록할 전역변수 설정
chunkLoadingGlobal: "webpackChunkwebpack", // string

// 엔트리포인트에서 사용할 수 있는 청크 로딩 타입 설정
// 웹팩에 의해 자동으로 설정됨
enabledChunkLoadingTypes: ["jsonp", "import-scripts"], // string[]

// 엔트리포인트에서 사용할 라이브러리 타입 설정
enabledLibraryTypes: [], // string[]

// 엔트리포인트에서 사용할 wasm 로딩 타입 설정
enabledWasmLoadingTypes: ["fetch"], // string[]

// 청크 포맷 설정
// web: array-push, worker: array-push, node.js: commonjs
chunkFormat: "array-push",

// HMR manifest 파일명 설정 (비권장)
hotUpdateMainFilename: "[runtime].[fullhash].hot-update.json", // string

// HMR 청크의 파일명 설정
hotUpdateChunkFilename: "[id].[fullhash].hot-update.js", // string

// HMR 청크를 로드할 시 JSONP 함수명 설정
hotUpdateGlobal: "webpackHotUpdatewebpack", // string

// 출력의 각 라인 앞에 붙을 prefix 를 설정
sourcePrefix: undefined, // string

// 사용할 해싱 알고리즘 설정
hashFunction: "md4", // string

// 해시를 생성할 때 사용할 인코딩 설정
hashDigest: "hex", // string

// 사용할 해시의 prefix 길이 설정
hashDigestLength: 20, // number

// 해시 솔트 설정 (해시 관련 이슈 발생시)
hashSalt: undefined, // string | Buffer.

// 워커 내에서 청크 로딩 방식 설정
workerChunkLoading: "import-scripts",

// 워커 내에서 wasm 로딩 방식 설정
workerWasmLoading: "fetch",
},

// 사용할 모듈 설정
module: {
// 모듈 규칙 설정
rules: [
{
// 조건
test: /\.jsx?$/,

// 포함할 경로 (exclude 보다 사용을 권장)
include: [path.resolve(__dirname, "app")],

// 제외할 경로 (test 보다 높은 우선순위)
exclude: [path.resolve(__dirname, "app/demo-files")],

// 어디서 import 되는지에 따라 모듈을 사용할지 설정
// 파일에 따라 raw-loader, babel-loader 등 import 방식을 다르게 쓰는 경우 사용한다.
issuer: { or: [/\.css$/, path.resolve(__dirname, "app")] },

/* 고급 조건 설정 */
// 모듈의 리소스와 일치하는지 테스트 (test, include 와 동일)
resource: /\.css$/,

// 하위 컴파일러 이름과 일치하는지 테스트
compiler: /html-webpack-plugin/,

// dependency 타입이 일치하는지 테스트
dependency: "esm", // import-style dependencies
dependency: "commonjs", // require-style dependencies
dependency: "amd", // AMD-style dependency
dependency: "wasm", // WebAssembly imports section
dependency: "url", // new URL(), url() and similar
dependency: "worker", // new Worker() and similar
dependency: "loader", // this.loadModule in loaders

// package.json 의 정보와 일치하는지 테스트
descriptionData: {
type: "module",
},

// 리소스의 mimetype 이 일치하는지 테스트
mimetype: "text/javascript",

// resource 와 같지만 리소스명이 변경된 경우 무시
realResource: /\.css$/,

// 리소스의 Fragment 가 일치하는지 테스트
resourceFragment: "#blah",

// 리소스의 쿼리스트링이 일치하는지 테스트
resourceQuery: "?blah",

// 적용할 로더를 설정
// use: [ { loader } ] 의 shortcut
loader: "babel-loader",
// 로더 옵션을 설정
options: {
presets: ["es2015"],
},

// 여러 로더를 한 번에 설정
use: [
"htmllint-loader",
{
loader: "html-loader",
options: {},
},
],

// 일치하는 모듈의 타입을 설정
// 설정 시 defaultRules 및 기본 import 기능은 우회된다.
// https://webpack.js.org/configuration/module/#ruletype
type: "javascript/auto", // 'javascript/auto' | 'javascript/dynamic' | 'javascript/esm' | 'json' | 'webassembly/sync' | 'webassembly/async' | 'asset' | 'asset/source' | 'asset/resource' | 'asset/inline'

/* 고급 액션 설정 */
// 로더 순서 설정
// 미설정시 normal loader 로 호출된다.
enforce: "pre", // "pre" | "post"

// 모듈 타입에 따른 제네레이터 설정
generator: {
dataUrl: {
encoding: "base64", // "base64" | false
mimetype: undefined,
},
// output.assetModuleFilename 를 override 하며 asset, asset/resource 타입의 경우만 동작
filename: "",
},

// 모듈 타입에 따른 파서 설정
parser: {
amd: false, // disable AMD
commonjs: false, // disable CommonJS
system: false, // disable SystemJS
harmony: false, // disable ES2015 Harmony import/export
requireInclude: false, // disable require.include
requireEnsure: false, // disable require.ensure
requireContext: false, // disable require.context
browserify: false, // disable special handling of Browserify bundles
requireJs: false, // disable requirejs.*
node: false, // disable __dirname, __filename, module, require.extensions, require.main, etc.
node: {
// reconfigure node layer on module level
},
worker: ["default from web-worker", "..."], // Customize the WebWorker handling for javascript files, "..." refers to the defaults.
},

// 모듈별 리졸브 설정
resolve: {
// 해당 key 를 리졸브 할시 script.js 로 대체
alias: {
key: "script.js",
},

// package.json 의 type: "module" 인 경우 파일 확장자와 파일명을 명시해야한다.
fullySpecified: true,
},

// 스코프를 벗어나 사이드이펙트를 발생시키는지 명시적으로 설정
// package.json 의 sideEffects 를 override
sideEffects: false, // boolean
},
{
// 일치하는 하나의 규칙만 사용
oneOf: [
// ... (rules)
],
},
{
// 중첩된 규칙 모두 사용
rules: [
// ... (rules)
],
},
],

/* 고급 모듈 설정 */
// 이 모듈에서 파싱하지 않을 경로 설정
noParse: [/special-library\.js$/],

// 동적 요청에 대한 모듈 컨텍스트 기본 설정
// 곧 deprecated 될 예정으로 사용 비권장
unknownContextRequest: ".",
unknownContextRecursive: true,
unknownContextRegExp: /^\.\/.*$/,
unknownContextCritical: true,
exprContextRequest: ".",
exprContextRegExp: /^\.\/.*$/,
exprContextRecursive: true,
exprContextCritical: true,
wrappedContextRegExp: /.*/,
wrappedContextRecursive: true,
wrappedContextCritical: false,
},

// 모듈 리졸브 설정
// (로더 리졸브 시에는 사용되지 않음)
resolve: {
// 모듈을 찾을 디렉토리
// 상대 경로일 경우 현재 디렉토리와 부모 디렉토리까지 확인
modules: ["node_modules"],

// 사용할 확장자
// 이름이 같고 확장자만 다를 경우 첫 번째 확장자를 사용
extensions: [".wasm", ".mjs", ".js", ".json"],

// 특정 모듈을 더 쉽게 리졸브하기 위해 별칭 설정
alias: {
// e.g. "module/path/file" -> "new-module/path/file"
module: "new-module",

// e.g. "only-module" -> "new-module", "only-module/path/file" -> "new-module/path/file" 는 불가
"only-module$": "new-module",

// e.g. "module" -> "./app/third/module.js", "module/file" 은 에러
module: path.resolve(__dirname, "app/third/module.js"),

// e.g. "module/file" -> "./app/third/file"
module: path.resolve(__dirname, "app/third"),

// e.g. "./app/module.js" -> "./app/alternative-module.js"
[path.resolve(__dirname, "app/module.js")]: path.resolve(
__dirname,
"app/alternative-module.js",
),
},

/* 고급 리졸브 설정 */
// package.json 의 imports, exports 에 사용되는 조건
conditionNames: ["webpack", "production", "browser"],

// 서버 관련 요청이 리졸브되는 경로
// context 가 기본값이며 요청이 절대 경로로 리졸브 되지 않는 경우만 동작한다.
roots: [context],

// 리졸브 실패시 모듈 fallback
fallback: { events: path.resolve(__dirname, "events.js") },

// 패키지를 가져올 때 package.json 에서 검사할 main 필드 설정
mainFields: ["main"],

// 리졸브 경로 제한
restrictions: [/\.js$/, path.resolve(__dirname, "app")],

// 리졸브 캐시
cache: false,

// 공격적이지만 안전하지 않은 리졸브 캐시
// 라이브러리가 안정적인 경우 퍼포먼스 향상이 가능하다고 한다.
unsafeCache: false,
unsafeCache: {},

// 리졸브용 플러그인
plugins: [
// ...
],

/* 전문가용 리졸브 설정 */
// 심볼링 링크일 경우 실제 경로로 확인
// 심볼릭 사용하지 않을 경우 false 가 성능에 좋다.
symlinks: true, // boolean

// package description 에 사용할 json 파일 경로
descriptionFiles: ["package.json"],

// package.json 에서 읽을 속성
// https://github.com/defunctzombie/package-browser-field-spec
aliasFields: ["browser"],

// 외부 요청을 위해 확인할 필드
// https://webpack.js.org/guides/package-exports/
exportsFields: ["exports"], // (default)

// 내부 요청을 위해 확인할 필드
importsFields: ["imports"], // (default)

// 디렉토리를 리졸브할 때 사용할 파일
mainFiles: ["index"],

// package.json 의 type: "module" 인 경우 파일 확장자와 파일명을 명시해야한다.
fullySpecified: true, // boolean

// 모듈 리졸브를 상대경로로 요청
preferRelative: true, // boolean

// 리졸브에 확장자 강제
enforceExtension: false, // boolean

// 리졸브 캐싱 필터
cachePredicate: ({ path, request }) => true,

// context 정보를 캐시키에 포함
// false 가 성능에 좋다.
cacheWithContext: false, // boolean

// 비동기 fs 대신 동기 fs 사용
useSyncFileSystemCalls: false, // boolean

// issuer 에 따라 리졸브 옵션 설정
// https://github.com/webpack/webpack/blob/master/lib/config/defaults.js#L992-L1009
byDependency: {},
},

// 웹팩 퍼포먼스 힌트 표시 설정
performance: {
// 힌트설정
hints: "warning", // "warning" | "error" | false

// 경고를 내보낼 최대 에셋 크기
maxAssetSize: 250000, // number

// 경고를 내보낼 최대 엔트리 크기
maxEntrypointSize: 250000, // number

// 퍼포먼스 힌트를 계산할 파일 필터 설정
assetFilter: (assetFilename) => {
return !/\.map$/.test(assetFilename);
},
},

// 브라우저 devtools 에 대한 소스맵 스타일 설정
// 설정에 따라 빌드 성능에 영향을 미칠 수 있다.
// https://webpack.js.org/configuration/devtool/#devtool
devtool: false, // enum, 위 링크 참조

// 설정에서 엔트리 및 로더를 확인하기 위한 기본 홈 경로 (절대경로)
context: __dirname, // string

// 번들이 실행되어야할 환경 설정
// web 이 기본이며 browserslist 환경에서는 browserslist 가 기본이다.
// https://webpack.js.org/configuration/target/#string
target: "web", // enum

// 번들링시 해당 모듈의 종속성을 제거한다.
// 주로 외부 라이브러리 종속성 제거에 사용된다.
// https://webpack.js.org/configuration/externals/#combining-syntaxes
externals: undefined, // string | [string] | object | function | RegExp

// externals 타입 설정
externalsType: "var", // 기본값은 output.library.type

// 특정 대상에 대한 externals 프리셋을 활성화한다.
externalsPresets: {
electron: false,
electronMain: false,
electronPreload: false,
electronRenderer: false,
node: false,
nwjs: false,
web: true,
webAsync: true,
},

// 경고를 무시할 패턴 설정
ignoreWarnings: undefined, // RegExp | (WebpackError, Compilation) => boolean | {module?: RegExp, file?: RegExp, message?: RegExp}

// 통계 설정
stats: "errors-only",
stats: {
// 프리셋
preset: "errors-only", // "error-only" | "error-warnings" | "minimal" | "none" | "normal" | "verbose" | "detailed"

/* 고급 전역 설정 */
// 옵션이 설정되지 않은 경우 대체값
all: false,

// 색상 설정
colors: true,

// 상대경로 표시를 위해 context 디렉토리 설정
context: "../src/",

// 출력에 모듈 및 청크 id 포함
ids: true,

// 출력에 env 포함
env: true,

// 출력에 절대 경로 포함
outputPath: true,

// 출력에 publicPath 포함
publicPath: true,
// include public path in the output

// assets 목록 표시
assets: true,

/* 고급 에셋 설정 */
// 에셋 정렬 설정
// !size 처럼 역순 가능
assetsSort: "id",

// 표시될 에셋 라인
assetsSpace: 15,

// 캐시된 에셋에 대한 정보 포함
cachedAssets: true,

// 제외할 에셋 경로
excludeAssets: false, // string | RegExp | (assetName) => boolean

// 에셋을 출력 경로별로 그룹화
groupAssetsByPath: true,

// 에셋을 확장자별로 그룹화
groupAssetsByExtension: true,

// 에셋을 상태별로 그룹화 (emitted, compared for emit, cached)
groupAssetsByEmitStatus: true,

// 에셋을 청크별로 그룹화
groupAssetsByChunk: true,

// 에셋을 정보별로 그룹화 (immutable, development, hmr 등)
groupAssetsByInfo: true,

// 관련 에셋 정보 포함 (sourcemap, compressed version 등)
relatedAssets: true,

// 퍼포먼스 힌트 포함
performance: true,

// 엔트리포인트 포함
entrypoints: true,

// namedChunkGroups 에 대한 정보 포함
chunkGroups: true,

/* 고급 청크 그룹 설정 */
// 엔트리포인트, 청크 그룹에 대해 보조 에셋 포함
chunkGroupAuxiliary: true,

// 하위 청크 그룹 포함 (prefetched, preloaded)
chunkGroupChildren: true,

// 청크 그룹 에셋 목록 제한
chunkGroupMaxAssets: 5,

// 청크 목록 표시
chunks: true,

/* 고급 청크 설정 */
// 청크 정렬 설정
chunksSort: "id",

// 빌드된 모듈에 대한 정보를 청크에 포함
chunkModules: true,

// 청크 출처 포함
chunkOrigins: true,

// 청크 관계 포함 (parents, children, sibilings)
chunkRelations: true,

// 청크 종속성 포함
dependentModules: true,

// 모듈 목록 표시
modules: true,

/* 고급 모듈 설정 */
// 표시될 모듈 라인
modulesSpace: 15,

// 중첩 모듈 포함
nestedModules: true,

// 캐시된 모듈 포함
cachedModules: true,

// 최적화 그래프에서 참조되지 않는 모듈 포함
orphanModules: false,

// 제외할 모듈 경로
excludeModules: false, // string | RegExp | (assetName) => boolean

// 모듈이 포함된 이유 추가
reasons: true,

// 모듈의 소스코드 포함
source: false,

/* 전문가용 모듈 설정 */
// 모듈 정렬 설정
modulesSort: "id",

// 모듈을 경로별로 그룹화
groupModulesByPath: true,

// 모듈을 확장자별로 그룹화
groupModulesByExtension: true,

// 모듈을 속성별로 그룹화 (errors, wanings, assets, optional, orphan, dependent)
groupModulesByAttributes: true,

// 모듈을 캐시 상태별로 그룹화
groupModulesByCacheStatus: true,

// 각 엔트리에서의 depth 포함
depth: false,

// 모듈 내 에셋에 대한 정보 포함
moduleAssets: true,

// 런타임 모듈에 대한 정보 포함
runtimeModules: true,

/* 고급 최적화 설정 */
// 모듈 exports 포함
providedExports: false,

// 사용되는 모듈의 exports 포함
usedExports: false,

// bailout 사유 포함
// https://webpack.js.org/plugins/module-concatenation-plugin/
optimizationBailout: false,

// chilren 정보 포함
children: true,

// 로그 레벨
logging: true,

// 특정 로거의 디버그 정보 포함
loggingDebug: /webpack/,

// 에러 스택 포함
loggingTrace: true,

// 경고 표시
warnings: true,

// 에러 표시
errors: true,

// 세부 에러 표시
errorDetails: true,

// 에러 스택 표시
errorStack: true,

// 에러와 관련된 모듈 스택 포함
moduleTrace: true,

// 빌드 시간 표시
builtAt: true,

// 에러 카운트 표시
errorsCount: true,

// 경고 카운트 표시
warningsCount: true,

// 빌드 소요시간 표시
timings: true,

// 웹팩 버전 정보 포함
version: true,

// 컴파일 해시 포함
hash: true,
},

// https://webpack.js.org/configuration/dev-server/
devServer: {
// 백엔드 개발 서버 프록시
proxy: {
"/api": "http://localhost:3000",
},

// static 파일 경로
// 절대 경로 사용 권장
contentBase: path.join(__dirname, "public"), // boolean | string | array

// gzip 설정
compress: true,

// history api 사용 시에 index.html 을 fallback 으로 설정
historyApiFallback: false,

// HMR 활성화
hot: true,

// 개발 서버를 https 로 서빙
// key, cert, ca 설정 필요
https: false,

// hot reload 시 에러와 경고만 표시
noInfo: true,
// ...
},

// 실험 기능 설정
experiments: {
// wasm 모듈을 비동기로 설정
// https://github.com/WebAssembly/esm-integration
asyncWebAssembly: true,

// deprecated (webpack4)
syncWebAssembly: true,

// ES module 허용
// output.libraryTarget 을 module 로 설정
outputModule: true,

// top-level await 를 허용
topLevelAwait: true,
},

// 빌드시 사용할 플러그인 설정
plugins: [
// ...
],

// 최적화 설정
optimization: {
// 청크 아이디를 생성할 때에 사용할 알고리즘
// production: "deterministic", development: "named", fallback: "natural"
// https://webpack.js.org/configuration/optimization/#optimizationchunkids
chunkIds: "deterministic", // false | "natural" | "named" | "size" | "total-size" | "deterministic"

// 모듈 아이디를 생성할 때에 사용할 알고리즘
// production: "deterministic", development: "named", fallback: "natural"
moduleIds: "deterministic", // false | "natural" | "named" | "deterministic"

// exports 명을 mangle 할지 설정
// production: "deterministic", fallback: false
mangleExports: "deterministic", // false | "deterministic" | "size"

// 출력 파일을 압축할지 설정
// production: true, fallback: false
minimize: true, // boolean

// 사용할 압축 플러그인 설정
minimizer: [],

/* 고급 최적화 */
// concatenate multiple modules into a single one
// production: true, fallback: false
concatenateModules: true, // boolean

// 빌드 에러가 있어도 출력을 내보낼지 설정
// production: false, fallback: true
emitOnErrors: false, // boolean

// 이미 로드된 청크에 포함되어있을 경우 청크를 다운로드하지 않게 플래그 설정
// production: true, fallback: false
flagIncludedChunks: true, // boolean

// 사용하지 않는 exports 에 대해 내부 그래프 분석 수행 설정
// production: true, fallback: false
innerGraph: true, // boolean

// 동일한 모듈을 포함하는 청크를 병합하게 설정
mergeDuplicateChunks: true, // boolean

// 웹팩 process.env.NODE_ENV 설정
// mode 값을 바라보고, mode: "none" 일 경우 false 와 동일
nodeEnv: "production", // string | boolean

// 레코드 생성시 상대경로를 사용할지 설정
// recordsPath, recordsInputPath, recordsOutputPath 사용시에 자동으로 활성화
portableRecords: false, // boolean

// 모듈에서 export * from 구문에 대해 효율적인 코드를 생성하게 설정
providedExports: true, // boolean

// 사용하지 않는 exports 를 제거
// production: true, fallback: false
usedExports: true, // boolean | "global"

// 파일 내용에 기반하여 contenthash 계산
// production: true, fallback: false
realContentHash: true, // boolean

// 모듈이 이미 상위 청크에 포함되어 있을경우 감지하여 제거
// 빌드 성능을 위해서는 비활성화하는 것이 좋다.
removeAvailableModules: false, // boolean

// 빈 청크파일 제거
removeEmptyChunks: true,

// 런타임 청크 설정
// 다중 엔트리의 경우 "single" 로 변경 후 런타임 청크를 공유할 수 있다.
runtimeChunk: false, // object | string | boolean

// exports 를 중복으로 사용할 때에 사이드이펙트가 없는 모듈 건너뛰기
// optimization.providedExports 가 활성화되어야 사용 가능
// production: true, fallback: "flag"
sideEffects: true,

splitChunks: {
cacheGroups: {
// 모듈별 세부 캐시 설정
"my-name": {
test: /\.sass$/,
type: "css/mini-extract",

/* 고급 셀렉터 */
chunks: "async",
minChunks: 1,
enforceSizeThreshold: 100000,
minSize: 0,
minRemainingSize: 0,
usedExports: true,
maxAsyncRequests: 30,
maxInitialRequests: 30,

/* 고급 이펙트 설정 */
maxAsyncSize: 200000,
maxInitialSize: 100000,
maxSize: 200000,
filename: "my-name-[contenthash].js",
idHint: "my-name",
name: false,
hidePathInfo: true,
automaticNameDelimiter: "-",
},
},

fallbackCacheGroup: {
automaticNameDelimiter: "-",
minSize: 20000,
maxAsyncSize: 200000,
maxInitialSize: 100000,
maxSize: 200000,
},

/* 고급 셀렉터 설정 */
// 최적화할 청크 선택
chunks: "all", // "async" | "all" | "initial"

// exports 명을 mangle 하거나 사용하지 않는 exports 를 삭제하기 위하여
// exports 를 분석할지 설정
usedExports: true,

// 모듈이 가져야할 최소 청크 수
minChunks: 1,

// 스플리팅이 강제되고
// minRemainingSize, maxAsyncRequests, maxInitialRequests 가 무시되는 사이즈 임계치
enforceSizeThreshold: 50000,
// ignore when following criterias when size of modules is above this threshold

// 생성할 청크의 최소 바이트
minSize: 20000,

// 남아있을 청크의 최소 바이트
// development: 0, production: minSize
minRemainingSize: 20000,

// 온디맨드 로드 시에 최대 병렬 요청 수
maxAsyncRequests: 30,

// 엔트리포인트의 최대 병렬 요청 수
maxInitialRequests: 30,

/* 고급 이펙트 설정 */
// 아래 사이즈보다 더 큰 사이즈를 스플리팅하여 청크 생성
// 우선순위: minSize > maxSize > maxInitialRequest === maxAsyncRequests
// 온디맨드만 적용
maxAsyncSize: 200000,
// 초기 로드 청크에만 적용
maxInitialSize: 100000,
maxSize: 200000,

// 청크 파일명 설정
filename: "[contenthash].js",

// 청크명 설정
// production: false 권장
name: false, // false | string | (module, chunks, key) => string

// maxSize 로 스플리팅된 청크에서 경로 노출 방지
hidePathInfo: true,

// 청크명에 들어갈 구분자
// e.g. vendor~main.js
automaticNameDelimiter: "~",

/* 전문가용 설정 */
// 사이즈를 설정할 때에 사용할 사이즈 유형 설정
defaultSizeTypes: ["javascript", "unknown"],
},
},

/* 고급 설정 */
// 로더 컨텍스트에 사용자 정의 API 또는 속성 추가
loader: {
/* ... */
},

// 로더에 대한 별도의 리졸브 옵션
// 웹팩의 로더 패키지를 확인하는 데만 사용
resolveLoader: {
/* same as resolve */
},

// node.js 기능 폴리필, 모킹 추가
node: {
// global 을 output.globalObject 로 치환
// 전역 변수가 필요한 모듈이라면 ProvidePlugin 를 권장
// https://nodejs.org/api/globals.html#globals_global
global: true, // boolean

// https://webpack.js.org/configuration/node/#node__filename
__filename: "mock", // boolean | "mock" | "eval-only"
__dirname: "mock", // boolean | "mock" | "eval-only"
},

// 빌드 간 모듈이 변경되는 방식을 추적하기 위해 레코드 JSON 파일 생성
recordsPath: path.resolve(__dirname, "build/records.json"),
recordsInputPath: path.resolve(__dirname, "build/records.json"),
recordsOutputPath: path.resolve(__dirname, "build/records.json"),

/* 고급 캐시설정 */
// 캐시 설정
// 개발 모드에서는 cache: true 이며 { type: "memory" } 와 동일
// 프로덕션 모드에서는 비활성화
// https://webpack.js.org/configuration/other-options/#cache
cache: false, // boolean | object
cache: {
type: "filesystem", // "memory" | "filesystem"

// 캐시 기본 폴더 설정
cacheDirectory: "node_modules/.cache/webpack", // string

// 캐시 경로 설정
cacheLocation: path.resolve(cache.cacheDriectory, cache.name), // string

// 무효화를 위한 캐시 의존성 추가
buildDependencies: {
defaultWebpack: ["webpack/lib"],
// 최신 웹팩 설정에 대한 캐시 의존성을 설정하려면 아래 설정 권장
// config: [ __filename ],
},

// 캐시에서 사용할 해시 알고리즘 설정
hashAlgorithm: "md4", // string

// 캐시명 설정
// 여러 웹팩 설정별로 독립된 캐시를 가져야할 때 변경할 수 있다.
name: `${config.name}-${config.mode}`, // string

// 파일시스템에 캐시를 저장할 시점 설정
// pack: 컴파일러가 idle 상태일 경우 단일 파일에 데이터 저장
store: "pack", // "pack"

// 파일 캐시를 무효화하기 위한 버전 설정
version: "", // string

// store: pack 인 경우 캐시를 저장할 주기 설정
idleTimeout: 10000, // number (ms)

// store: pack 인 경우 캐시를 초기화할 시간 설정
idleTimeoutForInitialStore: 0, // number (ms)
},

// 파일시스템 스냅샷을 생성하고 무효화하는 방법 설정
snapshot: {
// package.json 에서 관리되는 경로
managedPaths: [path.resolve(__dirname, "node_modules")], // string[]

// immutable 하여 스냅샷일 필요가 없는 경로
// path.resolve(__dirname, ".yarn/cache")
immutablePaths: [], // string[]

// 모듈 빌드시의 스냅샷 설정
module: {
// 타임스탬프를 비교하여 무효화 확인
timestamp: true,
// 해시 비교로 무효화 확인
// timestamp 보다 무겁지만 자주 변경되지 않음
hash: true,
},

// 리졸브시 스냅샷 설정
resolve: {
timestamp: true,
hash: true,
},

// 캐시를 사용시 빌드 종속성 리졸브시 스냅샷
resolveBuildDependencies: {
timestamp: true,
hash: true,
},

// 캐시 사용시 빌드 종속성 스냅샷
buildDependencies: {
timestamp: true,
// CI 환경에 적합
hash: true,
},
},

// watch 설정
watch: true, // boolean

// watch option 설정
watchOptions: {
// 파일 변경시에 지연시간 설정
aggregateTimeout: 200, // number (ms)

// watch 를 하지 않을 경로 설정
ignored: /node_modules/, // RegExp | string | [string, RegExp]

// poll 방식으로 watch 할지 설정
// 주로 nfs 사용으로 파일시스템에서 변경을 감지할 수 없을 경우
poll: false, // boolean | number (ms)
},

/* 고급 빌드 설정 */
// 인프라 수준 로깅 설정
infrastructureLogging: {
level: "info", // "none" | "error" | "warn" | "info" | "log" | "verbose"
debug: undefined, // true | string | RegExp | (name) => boolean | [string, RegExp, (name) => boolean]
},

// 병렬 처리할 모듈의 수 제한
// 성능을 미세하게 조정하거나 안정적인 결과를 얻는 데에 사용 가능
parallelism: 100, // number

// 통계 및 힌트를 포함하여 분석 도구에서 사용할 수 있게 프로필 제한
// 더 나은 결과를 위해 parallelism: 1 로 설정해야한다.
profile: true, // boolean

// 첫 오류 발생시 종료 설정
// 웹팩은 HMR 사용 시에 브라우저 콘솔, 터미널에 오류를 기록하지만 번들링을 게속하는데 이를 방지한다.
bail: false, // boolean

// 여러 웹팩 설정에 대한 빌드 의존성 설정
dependencies: ["name"],
};

기본 값 확인

참조

RxJS 병렬 HTTP 요청

· 약 3분

개요

  • 앵귤러 같기도 하고 코드를 머리를 써서 읽어야한다는 것 때문에 최대한 안 쓰고 싶다.
  • 하지만 async/await 로 동시성 제어가 힘드므로 이 쪽이 답인 것 같다.

소스

  • 데이터 배열에서 5개씩 끊어서 병렬 요청한다.
  • delay, catchError, delayWhen 의 조건만 다르게 하여 사용하면 된다.
import { EMPTY, from, of } from "rxjs";
import {
catchError,
delay,
delayWhen,
finalize,
map,
mergeMap,
retry,
tap,
toArray,
} from "rxjs/operators";

// 슈도코드임
const fetchObservable = (data) => {
return from(
new Promise((resolve) => {
setTimeout(() => {
resolve({
data,
});
}, 300);
}),
);
};

const concurrency = 5;
const fetchConcurrently$ = from(YOUR_DATA).pipe(
mergeMap((token) => {
return fetchObservable(YOUR_DATA).pipe(
map(({ data }) => data),
delay(1000),
retry(1),
catchError(() => EMPTY),
);
}, concurrency),
map((data) => {
return data.id;
}),
toArray(),
delayWhen(() => Promise.resolve()),
finalize(() => console.log("done")),
);

fetchConcurrently$.subscribe((ids) => console.log(ids));

테스트

  • 테스트 시에 jest 에서 done callback 을 적절히 호출해주자.
import { TestScheduler } from "rxjs/testing";
import { mergeMap, map, toArray } from "rxjs/operators";
import { from } from "rxjs";

describe("fetchConcurrently$ observable", () => {
let testScheduler;

beforeEach(() => {
testScheduler = new TestScheduler((actual, expected) => {
expect(actual).toEqual(expected);
});
});

it("should produce expected output", () => {
testScheduler.run((helpers) => {
const { cold, expectObservable } = helpers;

// Mock fetchObservable function
const fetchObservable = (data) => {
return from([data]);
};

// Mock YOUR_DATA
const YOUR_DATA = [1, 2, 3, 4, 5];

const concurrency = 5;
const fetchConcurrently$ = from(YOUR_DATA).pipe(
mergeMap((token) => {
return fetchObservable(token).pipe(
map((data) => data),
// ... other operators
);
}, concurrency),
map((data) => data),
toArray(),
);

const expectedMarble = "(abcde|)";
const expectedValues = { a: 1, b: 2, c: 3, d: 4, e: 5 };

expectObservable(fetchConcurrently$).toBe(expectedMarble, expectedValues);
});
});
});

여담

  • marble 테스트도 작성해보고 싶었는데 레퍼런스로 삼을만한 문서를 못 찾아서 아쉽다.
    • GPT4가 위처럼 잘 짜줬다.
  • 매번 of 와 from 을 헷갈리는데, 전자는 하나씩이고 후자는 덩어리다.

자바스크립트 활성 객체와 함수

· 약 7분

활성 객체

  • Activation Object
  • 함수가 호출되면 활성 객체가 만들어진다. 이는 숨겨진 데이터 구조로 호출된 함수의 반환 주소와 실행에 필요한 정보를 저장하고 호출된 함수에 바인딩한다.
  • C와 같은 언어에서는 활성 객체가 스택에 저장되고 함수가 종료되고 반환되면 스택에서 제거하지만 자바스크립트에서는 활성 객체를 다른 객체와 동일하게 힙에 저장한다.
  • 함수가 종료된다고 활성 객체를 자동으로 비활성화하지 않는다.
  • 활성 객체는 해당 객체에 대한 참조가 있는 한 계속 살아있으며 다른 객체와 마찬가지로 가비지 컬렉터에 의해 처리된다.

활성 객체 내부 정보

  • 함수 객체에 대한 참조
  • caller 함수의 활성 객체에 대한 참조 (이 참조는 return 이 실행흐름을 caller 로 돌릴 때 사용된다.)
  • 함수 호출이 끝난 뒤 실행을 재개하기 위해 필요한 정보 (대개 함수 호출문 바로 다음 명령어의 주소)
  • 파라미터에 의해 초기화되는 함수 매개변수
  • undefined로 초기화된 함수 변수들
  • 함수가 복잡한 표현식을 계산하기 위해 임시로 사용하는 변수들
  • 함수 객체가 메소드로서 호출되었을 때 사용할 수 있는 this 참조

함수

함수 객체

  • 함수 객체는 다른 일반적인 객체와 마찬가지로 속성을 갖고 있으며 변경 가능하다.
  • 공유되는 변경 가능한 함수 객체가 취약점으로 사용될 수 있다. (프로토타입 오염 등)
  • 함수 객체는 prototype 이란 속성을 가지고 있고, prototype은 Object.prototype에 대한 delegation link와 constructor 속성을 가진 객체의 참조를 가진다.
    • constructor 속성은 함수 객체에 대한 역참조를 가지고 있다.
  • 함수 객체는 Function.prototype 에 대한 델리게이션 링크를 가지고 있어 applycall을 상속받는다.
  • 함수 객체에는 두 가지 숨겨진 속성이 있는데 다음과 같다.
    • 함수 실행 코드에 대한 참조
    • 함수 객체가 생성되는 시점에 활성화된 활성 객체에 대한 참조 클로져를 사용할 수 있게 해준다.

함수 호출

  • 파라미터 표현식 계산
  • 함수의 매개변수와 변수를 저장할 수 있는 충분한 크기의 활성 객체 생성
  • 호출된 함수 객체에 대한 참조를 새로운 활성 객체에 저장
  • 전달받은 파라미터를 새로운 활성 객체의 매개변수에 저장
    • 빠진 파라미터는 undefined로 간주, 남는 파라미터는 버림
  • 활성 객체의 모든 변수 값을 undefined로 할당
  • 함수 호출 명령어의 바로 다음 명령어를 활성 객체의 next instruction 필드 값으로 할당
  • 새로운 활성 객체의 caller 필드 값에 현재 활성 객체를 할당
    • 실제 호출 스택이 아닌 활성 객체의 연결된 목록이다.
  • 새로운 활성 객체를 현재 활성 객체로 지정
  • 호출된 함수 실행

꼬리 재귀

  • 파라미터 표현식 계산
  • 현재 활성 객체가 충분히 크다면 (일반적으로 충분히 큼)
    • 현재 활성 객체를 새로운 활성 객체로 사용
  • 아닌 경우
    • 함수 매개변수와 변수를 저장할 수 있는 충분한 크기의 활성 객체 생성
    • 새로운 활성 객체의 caller 필드 값에 현재 활성 객체 저장
    • 새로운 활성 객체를 현재 활성 객체로 지정
  • 전달받은 파라미터를 새로운 활성 객체의 매개변수에 저장
    • 빠진 파라미터는 undefined로 간주, 남는 파라미터는 버림
  • 활성 객체의 모든 변수 값을 undefined로 지정
  • 호출된 함수를 실행

활성 객체를 재사용하므로 재귀 함수 호출을 반복문만큼 빠르게 처리 가능하다. try 블록의 꼬리 호출은 최적화되지 않는다.

  • try는 실행 흐름을 caller의 catch문으로 변경해야하는데, 이러면 활성 객체가 최적화 될 수 없음
  • 내부에서 새로운 변수를 가지고 있다면 활성 객체가 최적화 될 수 없음

꼬리 호출 예시

; recursion
call func
return

; tail recursion
jump func
(function loop() {
// do some
if (done) {
return;
}

// do more
return loop();
})();
// ❌
function factorial(n) {
if (n < 2) {
return 1;
}

return n * factorial(n - 1);
}

// ❌
const value = func();
return value;

// ❌
// eslint-disable-next-line no-unreachable
return function () {};

// ✔️
function factorial2(n, result = 1) {
if (n < 2) {
return result;
}

return factorial2(n - 1, n * result);
}
// ✔️
// eslint-disable-next-line no-unreachable
return (function () {})();

자바스크립트 문자열

· 약 4분

문자열

0~65535 사이의 크기를 가지는 Immutable unsigned int16 배열이다.

유니코드

  • 모든 언어를 16bit 로 표시하는 것이 목적이였으나 후에 21bit로 바뀌었다.
  • 자바스크립트는 16비트로 표현하려던 시절에 설계되어서 간극이 있다.
  • 자바스크립트는 문자를 받아 코드 유닛코드 포인트 둘로 나눈다.
  • 유니코드는 1,114,112개의 코드 포인트를 정의하고 있으며 한 평면당 65,536 코드포인트, 총 17개의 평면으로 나뉜다.
  • 원래의 평면은 BMP (Basic Multilingual Plane)라고 불리우며 나머지 16개의 평면은 나중에 추가되었다.
  • BMP에 있는 코드 포인트는 하나의 코드 유닛으로 식별가능하므로 사용이 쉽지만 그 외의 문자는 써로게이트 페어를 사용해야한다.

써로게이트 페어

  • 써로게이트 페어는 두 개의 코드 유닛으로 구성되는데, 자바스크립트에서는 1024개의 상위 써로게이트 코드유닛과 1024개의 하위 써로게이트 코드유닛이 있다.
    • 상위 써로게이트 코드유닛: 0xD800 ~ 0xDBFF
    • 하위 써로게이트 코드유닛: 0xDC00 ~ 0xDFFF
  • 계산식은 SurrogatePair = ((High - 0xD800) * 0x400) + (Low - 0xDC00) + 0x10000;이다.
  • 따라서 😃는 0x1F603 이며 D83D + DE03 이다.
    • "\uD83D\uDE03" === "\u{1F603}"
    • String.fromCharCode(55357, 56835) === String.fromCodePoint(128515);
  • 비트 연산자로도 계산이 가능한데, 코드 포인트에서 0x10000 (65,536)을 빼고 상위 10bit에 0xD800 을 더한 값과 하위 10bit에 0xDC00을 더하면 된다.
  • 비트 연산의 계산식은 다음과 같다.
SurrogatePair = SurrogatePair - 0x10000;
High = 0xd800 + (SurrogatePair >> 10);
Low = 0xdc00 + (SurrogatePair & 0x3ff);
return String.fromCharCode(High, Low);

전세계 사람들과 대화하기 위해 필요한 문자 대부분을 BMP에서 찾을 수 있지만, 유니코드 프로그램을 만들 경우에는 써로게이트 페어를 항상 염두해야한다. Unicode-aware 로 적혀지는 듯 하다.

정규화

  • 유니코드 문자에서는 악센트나 기타 문자를 수정할 수 있는 조합 및 수정 문자, 쓰기 방향 제어 문자 등이 포함되어있다.
  • 동일한 문자처럼 보이더라도 실제로는 다를 수 있다.
// 예시 1
"S\u0307" === "Ṡ";
"S\u0307\u0323" === "Ṩ";

const s1 = "S\u0307\u0323"; // Ṩ, S + 윗 점 + 아랫 점
const s2 = "S\u0323\u0307"; // Ṩ, S + 아랫 점 + 윗 점

s1 === s2; // false
s1.normalize() === s2.normalize(); // true

// 예시 2
const umlaut = "\u00FC"; // ü
const u_diaeresis = "u\u0308"; // u + 분음부호 ü

umlaut === u_diaeresis; // false
umlaut.normalize() === u_diaeresis.normalize(); // true

WeakMap, WeakSet 예제

· 약 2분

항상 WeakMap, WeakSet을 어떤 방식으로 쓸까 많이 고민되었다. 참조가 없는 경우 메모리를 반환하는데에 이점이 있는데, 여러 활용방안이 있었다.

WeakMap

Cache

// 📁 cache.js
const cache = new WeakMap();

// calculate and remember the result
function process(obj) {
if (!cache.has(obj)) {
const result = /* calculate the result for */ obj;

cache.set(obj, result);
}

return cache.get(obj);
}

// 📁 main.js
let obj = {
/* some object */
};

const result1 = process(obj);
const result2 = process(obj);

// ...later, when the object is not needed any more:
obj = null;

Sealer

값을 봉인하고 box를 반환해 box object 전체가 와야만 내부값을 알 수 있게 해준다.

function sealerFactory() {
const weakMap = new WeakMap();
return {
sealer(obj) {
const box = Object.freeze(Object.create(null));
weakMap.set(box, object);
return box;
},

unsealer(box) {
return weakMap.get(box);
},
};
}

WeakSet

Circular references

// Execute a callback on everything stored inside an object
function execRecursively(fn, subject, refs = null) {
if (!refs) {
refs = new WeakSet();
}

// Avoid infinite recursion
if (refs.has(subject)) {
return;
}

fn(subject);
if (typeof subject === "object") {
refs.add(subject);
for (const key in subject) {
execRecursively(fn, subject[key], refs);
}
}
}

const foo = {
foo: "Foo",
bar: {
bar: "Bar",
},
};

foo.bar.baz = foo; // Circular reference!
execRecursively((obj) => console.log(obj), foo);

참조