본문으로 건너뛰기

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. EnviromnetRecord의 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 안에 가능했던걸까.

Flutter와 타 프레임워크 비교

· 약 1분

비교

-FlutterReact NativeIonic
언어Dart + FlutterJS + ReactJS
컴파일결과네이티브 앱부분적인 네이티브앱웹뷰
UI 컴포넌트 컴파일🙅‍♂️ (픽셀제어)🙆‍♂️🙅‍♂️
플랫폼모바일, 웹, 데스크탑모바일, 웹모바일, 웹, 데스크탑
벤더구글페이스북아이오닉

요약

  • 플러터는 화면의 픽셀을 직접 제어한다.
  • 웹으로 빌드시에 캔버스 위에 생성된다.

Webpack5 설정

· 약 31분

개요

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

전체 설정

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 을 헷갈리는데, 전자는 하나씩이고 후자는 덩어리다.

Chrome dev summit 2020 빠르게 훑기

· 약 11분

Performance (특히 CLS)와 Privacy 가 행사 전반에 녹아있었다. 가장 중요했던 건 이제 Web Vitals 수치가 SEO 에 반영이 된다는 것이다.

State of speed tooling

Field vs Lab

  • Lighthouse 지표가 올해 리뉴얼 되었는데 FCP, TBT, CLS 지표를 주로 보면 된다.
  • 이 값이 Core Web Vitals 지표로 사용된다.
  • 실험실 데이터와 필드 데이터가 있는데, 실험실 데이터로 향상을 시킨 뒤 필드 데이터로 확인하면 된다.
  • Third party 에 대한 측정도 추가되었다.
  • Thrid party facades 를 통해 iframe 등도 lazy load 해야한다.

Fixing common Web Vitals issues

const connection = navigator.connection.effectiveType;
const { width, height } = window.screen;
const debugInfo = { connection, widght, height };
sendToAnalytics({ cls, debugInfo });

// Ohter sources of info:
// window.scrollY
// LayoutShiftAttribution (part of the Layout Instability API)
// Debugging info specific to your app (e.g. tokens)

UX patterns optimized for Core Web Vitals

Prevent CLS

  • Placeholder 를 놓아 CLS 를 막아야한다는 내용이였다.
  • Click 해서 내용이 추가되는 경우도 마찬가지이다.

Exploring the future of Core Web Vitals

Core Web Vitals and SEO

  • 첫째 날의 가장 중요한 섹션이지 않을까 싶다. 21년 5월부터 랭킹에 적용된다.
  • Loading, Interactivity, Stability 가 중요하다.
  • SEO 에는 필드 데이터를 사용한다.
    • Field data: RUM, CrUX 랑 비슷하고, last ~month 사이의 기간동안 측정된 결과이다.
    • Lab test data: 테스트 데이터

Ranking factors

  • Ranking factor 는 위와 같다. Google 검색결과의 페이지 환경 이해를 참조하자.
  • Search Console 에서 Core Web Vitals report 데이터를 확인 가능하다.
  • 회사명 같은 특정한 경우는 속도가 Ranking factor 에 주는 영향력이 낮지만, best editor 처럼 여러 검색 결과가 나오는 경우에는 page experience 가 표시에 더 많은 영향을 준다.
  • Great relevant content > Page experience
  • Page experience 가 좋으면 유저의 이탈율이 24% 감소한다. (The Science Behind Web Vitals)

Beyond fast

  • 동적으로 로드되는 영역에 대해 content-visibility을 줘서 placeholder 를 구현할 수 있다.
.content {
content-visibility: auto;
content-intrinsic-size: 0 500px;
}

font metrics override descriptors

  • 운영체제와 브라우저에 따라 폰트가 다르게 보이는 것과 swap 폰트 로드로 인해 레이아웃이 변경되는 구조를 막을 수 있다.
  • ascent-override, descent-override, line-gap-override Chrome87 부터 가능하다.
  • Back Forward Cache 가 Chrome87 에서 Same Origin 에 대해서도 적용되었다. (November 17, 2020)
  • chrome은 strict multi-process architecture 라 적용하는데 좀 걸렸다.
  • portal 을 18년 소개 후부터 미는데.. 브라우저 지원이 없어서 아직까진 잘 모르겠다.
  • 포탈을 쓰면 스토리지 제한이 없어서 두 사이트 간 content 공유가 쉽다.
  • quicklink 도 짧게 소개했다. newegg 사는 50% 컨버젼 상승과 4x faster navigation 을 달성했다고 한다.

Enable and debug cross-origin isolated

  • 헤더가 추가된다.
    • Cross-Origin-Opener-Policy: same-origin 코옵
    • Cross-Origin-Embedder-Policy: required-corp 코엡
  • Why you need "cross-origin isolated" for powerful features 문서와 Demo 참조하는 게 좋다.
  • COOP 는 no-openner 속성과 비슷하며 제어하는 위치의 차이이다.
  • COEP 는 CORP 와 함께 사용하여 리소스 로드를 제어할 수 있다. 서비스워커보다 상위에서 실행된다.
  • 요약하면 아래와 같다.
# 타 도메인 리소스
Cross-Origin-Resource-Policy: cross-origin
# 또는 CORS 열고 img[crossorigin] 추가

# 타 도메인 아이프레인
Cross-Origin-Resource-Policy: cross-origin

# 내 사이트
Cross-Origin-Embedder-Policy: require-corp
Cross-Origin-Opener-Policy: same-origin
# 또는 a[no-openner] 추가

Introducing the Privacy Budget

A more private way to measure ad conversions

Sign-up form best practices

  • 한 판을 정리해놓아서 이 것만 보면 된다.
  • 이름 검사에 정규식을 사용할 때 유니코드 기반을 사용해야한다. /[\p{L} ]+/u
  • 개인적으로 듣기 제일 좋았다.

SMS OTP form best practices

<input
type="text"
inputmode="numeric"
autocomplete="one-time-code"
pattern="\d{6}"
required
/>
  • 한 판을 정리해놓아 이 것만 보면 된다.
  • type="number" 가 아닌 type="text", inputmode="numeric" 을 사용해야한다.
  • OTP 문자 마지막 라인에는 @${bound-domain} #${OTP} 처럼 넣어주면 브라우저에서 파싱이 가능하다. (e.g. @gracefullight.dev #123456)
  • Web OTP API를 사용하여 서버에서 바로 받아서 사용할 수도 있다.

What's new in DevTools

  • Gird 지원 추가
  • Location 변경 기능 추가, Timezone, Language
  • Idle Detection API 추가, Kiosk 에서 비활성화시 사용 가능
  • Font-face 에서 fallback 을 확인 가능, Disable Local font 추가
  • animation 수정 가능
  • element screenshot
  • element hover 시 accessibility 확인 필드 추가
  • warning 등이 Issues 탭으로 나와서 가이드까지 추가 됨
  • WebAuthn 탭으로 디버깅 가능
  • CSS Overview 탭 추가
  • Media 디버깅 패널 추가

Extending CSS with Houdini

  • Houdini.how에 Worklet 확인 가능
  • CSS.paintWorklet.addModule 로 바로 가져와서 쓸 수 있다고 강조했다.
  • Static gradient worklet 가 예뻐보였다.

Debugging WebAssembly with modern tools

  • emcc 로 c++ 디버깅 옵션을 주면 c++ 소스를 바로 디버깅도 가능하다.
  • 추후 memory inspection, custom formatters, profiling, code coverage 기능을 추가할 예정이다.
  • google map 에 어떤 wasm 기술이 적용되었는지 알려주면 좋을텐데 아쉬웠다.

Transitioning to modern JavaScript

6x slower

  • 모던 자바스크립트는 최신 버전을 말하는 게 아니라, All modern browsers 가 사용할 수 있는 스크립트를 말한다.
  • ES2017 은 유저 중 94% 가 다 사용할 수 있으므로 더 높은 버전 사용시에는 이 버전을 타켓으로 해야한다. (ES2021은 70%)
  • 더 낮은 브라우저에 대한 지원이 필요할 경우 ESnext > ES2017 > ES2015 처럼 ES2017 을 한번 더 말아서 따로 제공해야한다. 필요 없는 코드가 너무 많이 들어가기 때문이다.
  • 달마다 80PB 가 필요없는 레거시 자바스크립트 로드에 사용된다고 한다.
  • EStimator 를 사용하여 어떤 코드를 변경해야할지 확인할 수 있다.
  • Publish, ship, and install modern JavaScript for faster applications

Next-level web apps on desktop

  • PWA 로 추가될 내용들이였다. 기능 설명 위주였다.
  • navigator.runOnOsLogin
  • Window placement window.getScreens(), internal/external
  • Tabbed application mode & Display override manifest.display_override: "tabbed"
  • Notification triggers TimestampTrigger
  • Link capturing manifest.capture_links, manifest.url_handlers
  • File type handling manifest.file_handlers, launchQueue
  • Badging (이게 드디어 나온다.)
  • Digital goods API

New logic to detect PWA offline support

  • 현재는 오프라인 체크시 서비스워커에 fetch event 가 정의되어있는지만 확인하는데 이제는 offine 시에 200 이 반환되어야한다.
  • chrome88 에서 check-offline-capability 플래그를 활성화하여 테스트 가능하다.
  • chrome89 에서 console.warn 을 발생시킬 것이다.
  • offline.html 파일을 강제시키는 bug fix 느낌이다.

What’s new for web apps in Play

  • bubblewrap/cli 사용하여서 TWA 를 만드는 방법
  • PaymentRequest 로 PWA 와 구글 결제 연동 가능

Extending Workbox: custom strategies and plugins

  • Workbox v6 이 되면서 Lifecycle hook 과 기존 strategy 를 확장 가능하게 변경되었다.
  • 한 판을 정리해놓아 이 것만 보면 된다.

Structured data for developers

  • Dynamic structred data 가 가능하다.
const s = document.createElement("script");
s.setAttribute("type", "application/ld+json");

fetch(`/api/events/structured-data?id=${id}`)
.then((r) => r.text())
.then((data) => {
s.textContent = data;
document.head.appendChild(s);
});
  • 테스트는 Rich Results Test 에서 가능하다.
  • schema-dts, react-schemaorg 패키지도 있다.

Making the web more visual with Web Stories

The web ahead

The web ahead

  • Wasm: The bridge that helps from lagacy application to that better world.
  • Write your code once for all browsers.
  • 원반 언제 다 볼 수 있을까 갈수록 기본기가 중요해지는 것 같다.

맥에서 블루투스 장치 차단하기

· 약 2분

블루투스 기기가 자동 연동될 때 쉘에서 직접 차단을 할 수 있다.

맥 어드레스 확인

option + shift 후에 블루투스 상태창을 클릭하면 MAC 주소(Media Access Control Address)를 알 수 있다.

주소 차단

블루투스 정보는 com.apple.Bluetooth.plist 에서 관리한다.

# 설정파일을 xml 형식으로 변환한다.
sudo plutil -convert xml1 /Library/Preferences/com.apple.Bluetooth.plist

# 변환한 파일을 열어 아래 라인에 맥 어드레스를 추가한다.
sudo vi /Library/Preferences/com.apple.Bluetooth.plist
2794 <key>IgnoredDevices</key>
2795 <array>
2796 <string>47-02-87-10-0f-34</string>
2797 <string>74-3a-4e-e3-5d-68</string>
2798 </array>

# 다시 바이너리로 바꾼다.
sudo plutil -convert binary1 /Library/Preferences/com.apple.Bluetooth.plist

이제 더 이상 모르는 기기로부터의 연결을 요청받지 않아도 된다.

참조

빅서로 업데이트 후부터 알 수 없는 맥 어드레스의 기기가 자동 연결되면서 매직 키보드와 트랙패드의 연결이 유실되는 현상이 있어서 무시하는 법을 찾기 시작하였는데, 비슷한 버그가 발생하는 것 같다.