본문으로 건너뛰기

외워서 푸는 알고리즘 테스트

· 약 4분

알고리즘 풀이 시 어느정도 외워두면 편한 패턴들이 있다. 코딩테스트 합격자 되기 도서를 참고하여 작성하였으며, 자바스크립트로 코딩테스트를 준비하는 분들이라면 해당 도서를 한번 쯤 읽어 봄직하다.

스택

  • 선입후출(FILO)
  • 최근에 삽입한 데이터를 대상으로 연산해야 한다면 스택을 떠올리는 것이 좋다.
  • 가장 가까운(최근)이라는 키워드를 보면 스택을 떠올려 봐야 한다.

  • 선입선출(FIFO)
  • 여러 이벤트가 발생했을 때 발생한 순서대로 처리할 때 큐가 활용된다.
  • 성능이 중요한 문제라면 큐를 직접 구현하거나 연결리스트를 직접 구현하여 푸는 것이 좋으나, 난이도가 높지 않다면 굳이 구현하지 않아도 통과할 수는 있다.
// 연결리스트
class Node {
constructor(data) {
this.data = data; // 요소의 값
this.next = null; // 다음 요소를 참조
}
}
class Queue {
constructor() {
this.head = null; // 첫 번째 요소 참조
this.tail = null; // 마지막 요소 참조
this.size = 0; // 큐의 길이
}
push(data) {
// 새로운 요소를 생성
const newNode = new Node(data);
if (!this.head) {
// 큐가 비어 있다면 head와 tail을 모두 새로 생성한 요소로 설정
this.head = newNode;
this.tail = newNode;
// 아니면 현재 tail의 next 속성을 새로운 요소로 설정 후 tail이 새로운 요소를 참조하도 록 변경
} else {
this.tail.next = newNode;
this.tail = newNode;
}
this.size++; // 큐 길이 증가
}
pop() {
// head가 null이라면 비어 있다는 뜻
if (!this.head) {
return null;
}
// 두 번째 요소를 head의 참조로 변경하면
// 자연스럽게 첫 번째 요소가 사라짐
const removeNode = this.head;
this.head = this.head.next;
// 만약 두 번째 요소가 없었다면
// 큐가 비어 있다는 뜻이니 tail도 null로 설정
if (!this.head) {
this.tail = null;
}
this.size--; // 큐 길이 감소
// 삭제된 요소의 값을 반환
return removeNode.data;
}

isEmpty() {
return this.size === 0;
}
}

해시

  • 해시 함수를 통해 키와 값으로 저장하여 빠른 데이터 탐색이 가능한 자료구조
  • 단방향으로 동작하기 때문에 키를 통해 값을 찾을 수는 있지만 값을 통해 키를 찾을 수는 없다.
  • 해시를 비교하는 함수도 O(n)이기 때문에 해시를 사용할 때라면 크게 문제가 되지 않는다.

이진 탐색

  • 정렬된 배열은 이진탐색을 통해 시간복잡도를 O(logN)으로 낮출 수 있다.
  • 배열의 절반을 나누어 중위 값이 목표값보다 크고 작은지를 비교하여 탐색한다.
function binarySearch(sortedArray, target) {
let left = 0;
let right = sortedArray.length - 1;

while (left <= right) {
let mid = Math.floor((left + right) / 2);

if (sortedArray[mid] === target) {
return mid; // 찾은 경우 해당 인덱스 반환
} else if (sortedArray[mid] < target) {
left = mid + 1; // 왼쪽 포인터를 오른쪽으로 이동
} else {
right = mid - 1; // 오른쪽 포인터를 왼쪽으로 이동
}
}

return -1; // 찾지 못한 경우 -1 반환
}

// 사용 예제
const sortedArray = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const target = 7;
const result = binarySearch(sortedArray, target);

if (result !== -1) {
console.log(`타겟 값 ${target}은(는) 인덱스 ${result}에 있습니다.`);
} else {
console.log('타겟 값을 찾지 못했습니다.');
}

그래프

DFS

깊이 탐색한 다음 되돌아오는 깊이 우선 탐색으로, 하나의 경로를 끝까지 시도해봐야 하는 문제에서 주로 사용된다. DFS에서 시간초과가 발생한다면 BFS를, BFS에서 메모리 초과가 발생한다면 DFS를 시도해보는 것이 좋다.

function solution(graph, start) {
const result = [];
const list = {};
const visited = new Set();
for (const [a, b] of graph) {
list[a] = list[a] ? list[a].concat(b) : [b];
}
function dfs(node, visited, result) {
visited.add(node);
result.push(node);
(list[node] || []).forEach(neighbor => {
if (!visited.has(neighbor)) {
dfs(neighbor, visited, result);
}
});
}
dfs(start, visited, result);
return result;
}

BFS

넓게 탐색하며 진행하는 너비 우선 탐색으로, 최단거리를 찾아야 하는 문제에서 주로 사용된다.

/** 넓이 우선탐색 */
function solution(graph, start) {
const result = [start];
const list = {};
const visited = new Set();
for (const [a, b] of graph) {
list[a] = list[a] ? list[a].concat(b) : [b];
}

const queue = [];
queue.push(start);
visited.add(start);
while (queue.length) {
const node = queue.shift();
for (const target of list[node] || []) {
if (!visited.has(target)) {
queue.push(target);
visited.add(target);
result.push(target);
}
}
}

console.log(result);
return result;
}

빌드시간 개선을 위한 CRA to Vite Migration

· 약 4분

현재 CRA를 기반으로 만들어진 패키지로 웹뷰 서비스를 배포하고 있는데요. 기존까지는 큰 문제가 없었지만, 클라우드 서버 내에서 빌드를 하게 되면서 빌드시간이 2~3배 증가하는 문제가 생겼습니다. 때문에 빌드시간 단축을 위해 CRA의 Webpack v5에서 Vite 로 Migration을 시도했는데요. CRA에서 Vite로 변경했을 때의 빌드시간과 함께 이때 발생한 문제점에 대해 공유드리려 합니다.

왜 Vite를 사용하나요? Vite는 사전빌드를 거친 후 Native ESM을 사용하여 배포하는데, 개발단계에서는 Go로 작성된 Esbuild를 사용해서 Webpack, Parcel과 같은 기존의 번들러 대비 10-100배 빠른 속도를 제공합니다. 다만, 확장성과 안정성을 위해 배포시에는 Rollup을 기반으로한 사전 빌드를 거칩니다. ⠀ CommonJS를 통하여 빌드할 뿐만 아니라 필요하지 않은 설정까지 포함된 CRA에 비해 Vite는 더 가볍고 빠르기 때문에 최근 CRA보다 Vite를 사용하는 추세입니다.


Vite를 사용하여 빌드한 결과

기존 CRA의 환경변수와 플러그인을 Vite로 변경하고, Vite Migration을 위해 몇가지 수정을 거친 후 테스트를 진행하였습니다.

그 결과 꽤 극적인 결과가 나타났습니다.

  • 기존 CRA의 Webpack을 사용한 빌드시간
    • 1 min 35 sec total from scheduled to completion.
  • Vite 변경 후 빌드시간 (기존 대비 약 67% 빠름) - 31 sec total from scheduled to completion. ⠀
  • 기존 CRA의 Webpack을 사용한 클라우드 서버 빌드시간
    • 4 min 8 sec total from scheduled to completion.
  • Vite 변경 후 클라우드 서버 빌드시간 (기존 대비 약 57% 빠름)
    • 1 min 47 sec total from scheduled to completion.

구형 브라우저 지원이 필수적인 웹뷰 환경

그러나 여기에는 한가지 간과한 부분이 있었습니다. Vite v5는 네이티브 ES 모듈네이티브 ESM의 동적 Import, 그리고 import.meta 을 지원하는 브라우저를 타깃으로 하고 있습니다. (Vite 브라우저 지원 현황)

하지만 웹뷰 특성상 여러 기기에서 사용하기 때문에 오래된 브라우저에 대한 지원이 필수적입니다. 실제로 간혹 Object.fromEntries 와 css gap 속성을 사용하지 못하는 기기로 인해 문제가 생기기도 했습니다.

때문에 @vitejs/plugin-legacy 를 사용하여 오래된 브라우저를 위해 폴리필 해줘야만 하는데요. last 2 versions and not dead, > 0.3% 으로 폴리필 한 결과 다음과 같은 결과가 나타났습니다.

  • Vite 구형 브라우저 폴리필 지원 후 빌드시간 (기존 대비 약 24% 빠름)
    • 1 min 12 sec total from scheduled to completion.
  • Vite 구형 브라우저 폴리필 지원 후 클라우드 서버 빌드시간 (기존 대비 약 79% 느림)
    • 7 min 24 sec total from scheduled to completion.

클라우드 서버에서 좋지 않은 결과를 보여주는 이유는?

Native ESM를 사용하는 Vite와는 달리, CRA의 Webpack은 CommonJS를 사용하고 있기 때문에 구형 브라우저에 대한 지원율이 높습니다. 하지만 Vite의 경우 더 많은 폴리필을 지원해야 하기 때문에 같은 커버리지를 지원하면서도 상대적으로 빌드 시간이 더 오래 걸리게 되는 것인데요.

그럼에도 로컬 빌드 시 조금 더 빠른 성능을 보여주는 이유는 Vite의 배포 빌드 시 사용하는 Rollup은 Rust로 쓰여진 SWC를 파서로 사용하고 있기 때문에(https://github.com/rollup/rollup/pull/5073)  성능이 좋은 멀티코어 환경에서는 조금 더 빠른 성능을 보여주지만, 성능이 좋지않은 클라우드 서버 환경에서는 매우 느린 결과를 보여주고 있다고 추측합니다.


결론

구형 브라우저까지 지원할 필요가 없는 환경이거나,  멀티코어 성능이 좋은 환경에서 빌드를 해야 한다면 Vite는 좋은 선택이 될 것 입니다. 다만 웹뷰와 같이 많은 사용자에 대한 지원이 필요한 환경이라면 빌드속도를 위해 Vite를 선택하는 것은 그리 효과적이지 않을 수 있습니다.

자바스크립트는 왜 프로토타입을 선택했을까

· 약 5분

자바스크립트의 프로토타입이란?

"프로토타입은 기존 객체를 복제하여 새로운 객체를 생성하는 디자인 패턴으로, 사전에 클래스를 정의해야 하는 클래스 디자인패턴에 비해 객체 생성과정을 단순화 할 수 있다는 장점이 있다." 이처럼 프로토타입 디자인패턴에 대한 설명으로 간단히 이야기할 수도 있다. 자바스크립트의 렉시컬스코프, 클로저, 호이스팅이 어떻게 생겨났는지를 프로토타입 철학을 기반으로 분석한 포스팅을 보고, 좀 더 쉽게 각색하여 서양철학부터 context까지를 간략히 정리하였다.

클래스와 인스턴스로 세상을 구분했던 19세기 이전 서양철학

객체지향 프로그래밍의 주요 개념인 클래스와 인스턴스의 최초의 구분은 플라톤에 의해 제시되었는데, 플라톤은 클래스과 인스턴스를 이분법적으로 명확히 구분했다. 눈앞에 실제로 존재하는 사물(인스턴스)이 있다면 반드시 그것의 본질(클래스)이 존재한다는 것이 플라톤의 주장이었다. 그리고 이러한 본질 세계를 이데아(Idea) 라고 칭하며 현실의 사물은 모두 이데아의 본질을 모방한 것이라 주장했다. 의자로 예를 들자면, 의자 라는 클래스가 있을 때 바퀴달린 의자는 의자 라는 클래스를 모방한(상속받은) 바퀴달린 의자라는 것이다.

그리고 이러한 철학을 물려받은 플라톤의 제자 아리스토텔레스는 세상에는 하나의 보편적인 분류가 있다고 말하며, 이를 통해 세상을 분류하려 하였다. 이때 속성을 가지고 세상을 분류하였는데, 이때문에 돌고래는 어류가 아닌 포유류로 분류되었다. 허파로 숨을 쉬고, 새끼를 낳아 기르기 때문이었다.

이분법적인 서양철학을 반박하는 비트겐슈타인과 prototype

하지만 우리의 일반적인 시각으로는 돌고래는 물에서 살고 헤엄을 치기 때문에 어류가 맞다고 생각할 수 있다. 이처럼 분류의 기준은 문맥에 따라서 달라질 수 있음을 주장하며 나온 것이 프로토타입의 기반이 되는 비트겐슈타인의 의미사용이론이다. 의미사용이론은 '벽돌!' 이라고 하는 말의 의미는 벽돌이 필요할 때에는 '벽돌을 달라!', 벽돌이 떨어질 때는 '벽돌을 조심해!' 와 같이 문맥에 따라 의미가 달라진다는 것이다.

또한 공통된 정의 특성이 없더라도 다양한 방식으로 닮아있을 수 있다는 “가족 유사성”이라는 개념을 정의했는데, 가족 구성원은 다양한 방식으로 서로 닮아 있는 것 처럼, 분류에는 '포유류는 젖을 먹고 허파로 숨을 쉬어야 한다.' 와 같은 공통된 속성이 필요하지 않다는 것이다.

이러한 비트겐슈타인의 의미사용이론, 가족 유사성은 1970년경 철학자 엘렌 로쉬 에 의해 프로토타입 이론(Prototype theory)으로 정리된다.

프로토타입 이론의 주요 특징 CONTEXT

엘렌 로쉬가 정리한 프로토타입 이론은

  • 같은 단어라도 어떤 상황(context)에서 접했냐에 따라 달라진다
  • 정의로 부터 분류되는 것이 아니라 가장 좋은 보기로부터 범주화 된다. 라는 특징을 가진다.

위 특징은 자바스크립트의 동작원리와 밀접한 관계를 가지는데, 자바스크립트의 특징인 스코프체인, 호이스팅, this, closure이 자바스크립트의 실행 컨텍스트(context)에서 비롯되기 때문이다.

즉, 자바스크립트가 이러한 실행컨텍스트를 가지는 이유는 문맥에서 비롯되는 객체의 의미를 정의하기 위해서이며, 프로토타입 기반의 언어인 자바스크립트가 가지는 특징이 되는 것이다.


3줄요약

  1. 프로토타입은 한가지의 보편적인 분류로 의미를 나누는 class와 반대되는 개념이다.
  2. 프로토타입 이론은 의미가 문맥(context)에 따라 달라질 수 있음을 주장하는 철학이다.
  3. 자바스크립트 프로토타입의 호이스팅, 클로저, 스코프체인 등은 이러한 프로토타입의 문맥(context)을 이해하기 위한 수단이다.


참고자료: 자바스크립트는 왜 프로토타입을 선택했을까 JavaScript : 프로토타입(prototype) 이해 ECMAScript 명세 이해, 1부 [JS] 📚 자바스크립트 실행 컨텍스트 원리

더 빠른 웹페이지를 만드는 방법::HTTP 압축 - webp, gzip, Brotli, gRPC

· 약 4분

좋은 사용자경험을 위해서는 가장 큰 요소(LCP)가 2.5초 이내 혹은, 의미있는 콘텐츠(FMP)가 1.8초 이내에 일어날 수 있도록 권장하고 있다. 최적화를 덮어놓고 개발을 하다보면 위에서 말한 임계치가 넘어서곤 하는데, 이때 웹페이지를 더 빠르게 할 수 있는 방법을 공유하려 한다.

어떻게 하면 더 빠르게 만들 수 있을까?

파일 포맷 압축

JPEG 파일 크기: 43.84KB

WebP 파일 크기: 29.61KB

webp

가끔 이미지를 다운받으면 볼 수 있는 webp 확장자는 2010년 구글에서 만든 이미지포맷으로, JPEG, PNG, GIF를 대체할 수 있도록 손실, 무손실 압축 방식을 모두 지원하며, 기존 포맷대비 약 30% 정도 압축률이 좋다. 때문에 자주 사용하는 이미지들은 webp 포맷을 사용하면 네트워크 속도를 높일 수 있다.


손실, 무손실 압축에 대해 짧게 설명하자면, JPEG와 같은 포맷은 사용자가 알아채기 힘들 정도의 데이터를 버리면서 압축하며, PNG와 같은 포맷은 데이터를 버리지 않고 압축한다. 오래된 고전 짤들이 여기저기 옮겨다니며 손실 압축이 반복되어 디지털 풍화가 생기기도 한다.

(디지털풍화 예시)


종단간 압축

웹서버에서 압축된 리소스를 보낸 후 브라우저에서 압축을 풀어 사용하는 방식이다. 압축된 더 작은 크기의 데이터를 보내면서 네트워크 요청과정을 빠르게 하는 것이다.

gzip

가장 흔히 사용하는 것은 gzip 방식으로, 최대 70%까지 데이터를 압축할 수 있다. 중복되고 필요없는 코드를 압축하여 동작하며, 압축단계를 조절할 수도 있다. 사용방식은 서버에서 nginx 혹은 apache 설정을 통해 비교적 간편하게 사용할 수 있다.

Brotli

구글에서 만든 압축 알고리즘인 Brotli은 gzip에 비해 최대 20% 더 압축할 수 있다고 한다. gzip과 마찬가지로 서버에서 nginx 혹은 apache 설정을 통해 사용할 수 있으며, ie에서는 동작하지 않는다.

gRPC

gRPC는 HTTP/2, Protocol Buffers를 사용하여 데이터를 압축하여 통신한다. HTTP/2 프로토콜을 통해 여러 요청을 동시에 처리할 수 있으며 ProtoBuf는 xml보다 3~10배 작다고 할 정도로 Protocol Buffers를 통해 데이터를 압축할 수 있다.

드라마틱하게 압축할 수 있는 이유는 위처럼 이진 데이터 구조를 사용하기 때문인데, 우리가 주로 사용하는 방식인 REST에서는 데이터를 주고받는 포맷으로 xml, json을 사용한다. 하지만 ProtoBuf는 '00001...'과 같이 이진 데이터 구조로 만들어 전달하고, 이진 데이터를 다시 변환하여 사용한다.

때문에 gRPC를 사용하기 위해서는 아래처럼 이진 데이터를 만들고 해제할 수 있는 포맷을 만들어야 하며, 데이터를 주고받는 동안 이진데이터 형식이기 때문에 사람이 읽을 수 없다는 단점이 있다.

message Person {
required string user_name = 1;
optional int64 favourite_number = 2;
repeated string interests = 3;
}

참고자료:

Compression in HTTP, google webp, Protocol Buffer 원리로 배우는 고성능 직렬화, 역직렬화 전략, [네이버클라우드 기술&경험] 시대의 흐름, gRPC 깊게 파고들기

렌더링이 얼마나 빨라요 하나요?::RAIL 성능모델

· 약 4분

프론트엔드 실무를 진행하다 보면 이런 말을 듣거나 해야할 때가 있다.
여기 좀 느린 것 같지 않아요?

개발환경에서는 문제없었던 부분들이 배포 후에 느리게 느껴져서 일 수도 있고, API 응답속도가 느려져 서버팀을 설득해야 할 때에도 있다. 그럴 때 마다 어느 기준에 맞춰야 하는지에 대해 어려울 수 있는데, RAIL 성능 모델을 기준으로 한다면 개발환경에서 속도를 측정하는데 꽤나 도움이 될 것이며, 다른 사람을 설득하는데에도 도움이 될 것이다.


RAIL은 Response, Animation, Idle, and Load 의 축약어로 2015년 사용자 경험을 위해 구글 크롬에서 만들어진 사용자 중심의 성능 모델이다. UX 연구에 기반하여 응답(Response)은 100ms, 애니메이션(Animation)은 16ms, 유휴(Idle)은 50ms, 로드(Load)는 5s 이내에 동작하는 것이 사용자 경험에 좋다는 것인데 각각의 내용을 좀 더 자세히 설명하자면,

Response

좋아요 버튼을 누른 후 약 0.5초 후에 좋아요 여부가 바뀌는 것이 답답한 느낌을 준다. 아무생각없이 개발하다보면 마주칠 수 있는 상황인데, 좋아요와 같이 즉각적으로 UI가 바뀌어야 하는 것은 사실 0.2초만 되어도 묘하게 답답하다는 느낌이 들 수 있다. 때문에 이러한 기능들을 개발할 때에는 변화가 필요한 곳만 렌더링 혹은 API 호출을 하거나, 낙관적 업데이트(Optimistic update)를 고려해보는 것이 좋다.

Animation

흔히 볼 수 있는 애니메이션을 가져왔는데, 프레임이 낮다보니 눈에 거슬릴 뿐 아니라 렉이 걸리고 있다는 느낌마저 든다. RAIL 모델에서는 60프레임을 1초로 나눈 0.016초 마다 렌더링 하기를 권장하고 있는데, 실제 브라우저에서 렌더링을 할 때에는 각 프레임에 대한 자체 오버헤드가 있기 때문에 10ms으로 구현하는 것이 좋다. [참고] 렌더링성능

Idle

웹서핑을 하다보면 쉽게 볼 수 있는 현상들인데, 다른 동작들로 인해 사용자의 응답이 지연되는 경우이다. (물론 위 이미지는 성능을 낮춰 일부러 구현한 현상으로, velog의 성능과는 무관하다.)

이러한 현상은 스크롤과 같이 이벤트가 자주 일어날 때 생길 수 있는데, 때문에 리액트를 사용한다면 스크롤 이벤트를 사용하기 보다는 intercepter observer 를 사용하는 것을 추천한다. 만약 이벤트 리스너를 사용해야 한다면 passive event listeners를 사용해서 Idle을 줄일 수 있다.

Load

상호작용할 수 있는 콘텐츠, 쉽게 말하면 클릭할 수 있는 콘텐츠가 5초 이내로 나타나야 한다는 것이다. 페이지의 핵심이 되는 이미지, 글과 같은 것이 나타나는 것이 중요하긴 하지만, 상호작용할 수 있는 콘텐츠가 나타나지 않는다면 사용자는 작업이 중단되었다고 인식한다는 것이다.

위 이미지에서는 대부분이 상호작용할 수 있는 콘텐츠로, 개인적으로는 Load 보다는 의미있는 콘텐츠(FMP)가 1.8s 안에 나타나야 한다는 것이 좀 더 실무에 가깝지 않을까 싶다.



끝으로,

RAIL 모델이라는 것이 엄청 유명하지는 않지만, 그래도 구글에서 나온 모델로, 좋은 사용자경험을 만들 때 뿐 아니라 권위자로 인한 설득력을 얻을 수 있다는 점에서 알아두면 좋은 모델인 듯하다. 사족으로, RAIL 모델을 알아보면서 MDN에 잘못된 정보가 있는 것을 발견하고 MDN contributor가 되는 좋은 경험을 하기도 했다.

전역상태를 사용하지 않고 props drilling을 없애보자::Observer Component

· 약 4분

섬세한 ISFP의 코드 가독성 개선 경험에서 가독성 향상을 위해 사용한 ObserverComponent을 통해 전역상태관리 라이브러리 의존 낮추기


요즘 개발을 할 때 편리함과 가독성이라는 이유로 recoil을 애용하고 있는데, 끝없이 늘어나는 atom과 리코일 import문을 보면서 전역상태관리 라이브러리에 너무 의존하고 있다는 생각이 들었다. 그러던 중 섬세한 ISFP의 코드 가독성 개선 경험에서 ObserverComponent를 보게 되었는데 영상에서 나온 패턴에 어떤 장점을 가지고 있는지 궁금했기도 했고, 전역상태관리를 어느정도 대체할 수 있을거라 생각되어 직접 구현해보았다.

Observer Component

Observer Component는 이벤트핸들러를 정의한 뒤, 하위 컴포넌트의 Observed Component에서 발생하는 이벤트 버블링을 캡쳐하여 정의한 이벤트핸들러를 실행하는 패턴이다.


구현내용은 다음과 같다.

  • HOC패턴을 사용하여 obsevedComponent를 만드는 Observer Component를 구현한다.
  • data-click-log와 data-click-param을 갖는 observered component를 생성한다.
  • App 컴포넌트에서 이벤트핸들러를 정의한다.
  • onClick 이벤트에서 id가 data-click인 경우, 해당 클릭이 일어난 components 정보와 이벤트 정보를 가져온다.
  • 해당 컴포넌트의 데이터를 바탕으로 정의된 이벤트를 실행한다

여기서 HOC(higher-order-components)는 컴포넌트를 가져와 새 컴포넌트를 반환하는 함수이며, 현대 React에서는 주로 사용되지는 않는다고 한다. higherOrderComponent


직접 만들며 느낀 장점은 이벤트 버블링을 이용하기 때문에 props를 전달하지 않아도 된다는 점, 단점은 최상위 컴포넌트에서 이벤트를 정의, 관리 번거로움이 있다는 것이 있었다.


구현 코드

타입스크립트를 사용하였고, 영상에서 모든 코드가 나오지 않기 때문에 임의로 작성/수정한 코드가 많다.

ObserberComponent

export default function ObserberComponent<P extends JSX.IntrinsicAttributes>(
Component: React.ComponentType<P>,
handleEvent: (event: { [key: string]: string } | string) => void
) {
function Observer(props: P) {
return (
<div
onClick={(e: React.MouseEvent<HTMLDivElement>) => {
const target =
((e.target as HTMLElement)?.closest(
'[data-click-log]'
) as HTMLElement) || null;
if (!target) return;
handleEvent(
extractParams({
el: target,
paramTarget: 'data-click-log',
paramLabel: 'data-click-param',
})
);
}}
>
<Component {...props} />
</div>
);
}
return Observer;
}

interface ExtractParams {
el: HTMLElement;
paramTarget: string;
paramLabel: string;
depth?: number;
}

/** @description i번째의 부모 엘리먼트까지 paramTarget 있는지 체크한 뒤 paramLabel으로 구별하여 객체를 반환한다. */
const extractParams = ({
el,
paramTarget,
paramLabel,
depth,
}: ExtractParams): { [key: string]: string } => {
if (!el) return {};
let paramEl = el;
const params = {};
const param = `[${paramTarget}]`;

for (let i = 0; i < (depth || 3); i++) {
const paramsEl = paramEl.closest(param);
if (!paramEl || !paramEl?.parentElement) break;
const targetAttribute = paramsEl?.getAttribute(paramTarget);
const labelAttribute = paramsEl?.getAttribute(paramLabel);
if (!targetAttribute || !labelAttribute) break;
Object.assign(params, { [labelAttribute]: targetAttribute });
paramEl = paramsEl?.parentElement!;
}
return params;
};

ObserverComponent를 정의하는 App 컴포넌트

const ObservedFirstPage = ObserberComponent(FirstPage, arg => console.log(arg));

function App() {
return (
<div>
<ObservedFirstPage />
</div>
);
}

export default App;

data-click-param과 data-click-log 속성을 가진 FirstPage

export default function FirstPage() {
return (
<div data-click-param="First Page Param" data-click-log="First Page Log">
FirstPage
<button type="button">Fist Page Button</button>
<SecondPage />
</div>
);
}

버튼 클릭 시 { First Page Param ****: "First Page Log" } console 출력


data-click-param과 data-click-log 속성을 가진 FirstPage 내부의 SecondPage

export default function SecondPage() {
return (
<div data-click-param="Second Page Param" data-click-log="Second Page Log">
SecondPage
<button type="button">Second Page Button</button>
</div>
);
}

버튼 클릭 시 {Second Page Param: 'Second Page Log', First Page Param: 'First Page Log'} console 출력

Vite에 대한 간단한 소개::CRA 대신 Vite를 사용해보는 것은 어떤가요?

· 약 3분

Vite 개요

빠르다는 의미의 프랑스어 vite는 바이트가 아닌 비트라고 읽는다. 이름과 같이 vite는 매우 빠른 속도를 보여주는데, Vite의 사전 번들링 기능은 Go 언어로 작성된 Esbuild을 사용하여 기존 Webpack, Parcel과 같은 번들러 대비 10-100배 빠른 속도를 가진다.


Vite가 생겨나게 된 배경

vite를 비롯하여 이렇게 빠른 속도를 가진 도구들이 출시될 수 있는 배경에는 메이저 브라우저 엔진들이 네이티브 자바스크립트 모듈을 지원하기 시작하면서이다.

브라우저에서 네이티브 자바스크립트 모듈을 지원하기 전까지는, JavaScript 모듈화를 네이티브 레벨에서 진행할 수 밖에 없었다. 때문의 개발자들은 번들링(Bundling)이라는 우회적인 방법을 사용할 수 밖에 없었다.

때문에 개발 서버를 실행할 때 오랜 시간이 걸릴 수 있으며, 편집한 코드가 브라우저에 반영되기 까지 수 초 이상의 시간이 소요되기도 했다.


Vite는 어떻게 빠른 속도를 가질 수 있는가?

vite는 이를 해결하기 위해 내용이 바뀌지 않을 Plain JavaScript 소스 코드를 Go로 작성된 Esbuild를 사용하여 사전번들링하고, JSX, CSS와 같이 컴파일이 필요하고 수정이 잦은 Non-plain JavaScript 소스 코드에 대해서는 Native ESM을 이용하여 코드를 편집하자마자 브라우저가 변경될 수 있도록 만들었다. (하지만 사전번들링 시 Esbuild는 아직 불안정하기 때문에 Rollup을 사용하고 있다.)

Native ESM은 소스코드를 제공하는 방식을 사용하는데, 개발 서버를 구동할 때, 애플리케이션 내 모든 소스 코드에 대해 크롤링 및 빌드 작업을 마쳐야지만이 실제 페이지를 제공할 수 있었던 번들러 기반의 도구와 달리, 브라우저가 삽입 구문을 찾고 HTTP로 해당 모듈을 요청하고, 요청된 모듈과 모듈의 가져오기 트리에 있는 모든 잎 노드에 변환을 적용한 다음 이를 브라우저에 제공하는 방식이다.


그렇다면 Vite에는 장점만 있는 것일까?

일반적으로 자주 사용하는 번들러인 CRA의 경우, jest, svg-loader 등이 기본적으로 설치되어 있으며 eslint, prettier 등을 바로 설치해서 사용할 수 있다.

하지만 Vite의 경우 jest, svg-loader 등을 하나하나 설치해줘야 하며 eslint, prettier, favicon 등을 사용할 때 플러그인을 별도로 설치해줘야 한다.

하지만 웹팩에 추가 설정을 하기 위해 craco와 같은 라이브러리로 조작해야하는 CRA에 비해 자체적으로 config를 수정할 수 있도록 지원하여 손쉬운 절대경로 설정이 가능하며 CRA의 대부분의 기능을 대체할 수 있으므로, 다음 프로젝트를 기획하고 있다면 CRA 대신 Vite를 사용해보는 것을 적극 권장한다.



참고자료:

vitejs-kr, TOAST UI https://wonillism.tistory.com/271