함수형도 싱글톤 되잖아요? 그럼에도 클래스를 택한 이유

“함수형도 싱글톤 만들 수 있는데요?”
맞습니다. 기술적으로 가능합니다.
하지만 저는 단순히 ‘싱글 인스턴스’를 만드는 것만으로는 부족했습니다.
본 글은 지난 “왜 클래스 기반 ZebraLabelService를 선택했는가” 글에 이은 후속 글 입니다.


함수형도 싱글톤이 될 수 있습니다

사실 함수형으로도 싱글톤은 만들 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 함수형 싱글톤 패턴 (즉시 실행 함수)
const PrinterService = (() => {
let isInitialized = false;

return {
async initialize() {
if (!isInitialized) {
// 초기화 작업
isInitialized = true;
}
},
async print() {
if (!isInitialized) throw new Error('초기화 필요');
// 인쇄 작업
},
cleanup() {
isInitialized = false;
},
};
})();

이렇게 구현하면 모듈 자체가 싱글 인스턴스처럼 동작하므로 기본적인 싱글톤 조건은 충족합니다.


하지만 ‘기술적으로 된다’와 ‘현실에 적합하다’는 다릅니다.

함수형 싱글톤도 구현은 되지만, 프린터와 같은 하드웨어 통합 환경에서는 아래와 같은 이유로 클래스 기반 설꼐가 더 적합했습니다.


클래스 기반이 더 적합했던 이유

1. 상태 은닉과 캡슐화의 명확성

  • 함수형에서는 상태가 클로저 안에 숨어있고, 테스트나 디버깅 시 추적이 어렵습니다.
  • 클래스에서는 상태가 명시적으로 필드로 존재하며, 추적과 주입이 쉽습니다.
1
2
3
4
// 클래스
class ZebraLabelService {
private browserPrint: ZebraBrowserPrintWrapper | null = null;
}

코드를 보는 순간, 어떤 상태를 갖고 있는지 명확합니다.

함수 vs 클래스


2. 의존성 주입 시점 통제

1
2
3
4
// 클래스에서는 메서드 단위 주입 가능
async initialize() {
this.browserPrint = new ZebraBrowserPrintWrapper(); // 여기서 주입
}
  • 생성자나 메서드에서 외부 객체를 주입받을 수 있어 Mock 테스트가 수월
  • 함수형은 클로저 내에서 의존성을 고정하거나 외부에서 따로 관리해야 함 → 유연성 떨어짐

3. 초기화 중복 방지(Promise 캐싱) 구조가 깔끔함

1
2
3
4
5
6
7
8
9
// 클래스 예시
async initialize(): Promise<boolean> {
if (this.initPromise) return this.initPromise;

this.initPromise = this.performInitialization();
const result = await this.initPromise;
this.initPromise = null;
return result;
}
  • 클래스에서는 비동기 초기화 중복 호출을 방지하는 로직을 인스턴스 내부에 안전하게 구현할 수 있음
  • 함수형은 별도 상태 관리 또는 모듈 스코프 변수가 필요 → 명확한 책임 분리가 어려움

4. 명확한 수명 주기 관리

1
2
3
4
5
// 클래스 기반 수명 주기
initialize()
ensurePrinterReady()
printSeparateLabels()
cleanup()
  • 클래스에서는 수명 주기 메서드가 한 객체 안에서 명확하게 정의되어 있어,
  • 사용하는 쪽에서는 사용 규약을 명확히 따를 수 있음

함수형 모듈은 내부에 상태를 숨기다 보면 초기화/해제 로직이 분산되기 쉽습니다.


5. 확장성과 인터페이스 구현의 유리함

  • 클래스 기반 서비스는 IPrinterService 등의 인터페이스 구현이 자연스럽고
  • 테스트 시 Mock 클래스로 교체하기도 용이합니다.
1
2
3
class MockZebraLabelService implements IPrinterService {
/* 테스트용 로직 */
}

→ 구조적 유연성에서 함수형보다 객체지향 방식이 확장에 유리


함수형 싱글톤은 ‘가능’, 클래스 기반은 ‘현실적’

비교 항목 함수형 싱글톤 클래스 기반 싱글톤
싱글 인스턴스 보장 가능 (모듈/클로저로) 가능 (static 필드)
상태 관리 클로저 기반, 추적 어려움 명시적 필드, 디버깅 용이
초기화/해제 관리 외부 분산될 수 있음 라이프사이클 메서드로 일관
의존성 주입 어려움 (클로저 바깥 주입 필요) 용이 (생성자/메서드 주입)
테스트/Mock 용이성 낮음 높음
설계 유연성/확장성 낮음 높음

결론

React 기반에서도 대부분 함수형으로 충분합니다.
하지만, 프린터처럼 하드웨어 리소스 통합과 상태 일관성이 중요한 환경에서는 단순히 ‘싱글 인스턴스를 만들 수 있다’는 수준의 함수형 접근은 한계가 있습니다.

클래스 기반 싱글톤은 기능뿐 아니라 책임, 수명 주기, 상태, 의존성까지 안전하게 감싸는 구조입니다.

이러한 이유로 우리는 함수형 싱글톤이 가능함에도 불구하고,
클래스 기반 싱글톤을 선택했습니다.


기억해두면 좋은 체크리스트

  • 함수형이 싱글톤 구현이 가능한 건 맞다
  • 하지만 클래스는 캡슐화, 라이프사이클, 테스트성까지 아우른다
  • 하드웨어 통합에서는 초기화/상태/의존성 관리가 핵심이다
  • 클래스 기반은 책임 분리가 명확하여 협업/유지보수에 유리하다

왜 클래스 기반 ZebraLabelService를 선택했는가

Zebra 라벨 프린터 통합: 왜 클래스 기반 ZebraLabelService를 선택했는가

“React 생태계에서 굳이 클래스를?”
하드웨어(프린터)를 다룰 때는 이야기가 달라집니다. 단일 연결, 복잡한 초기화, 상태/리소스 수명 주기… 이 모든 것을 안전하고 일관되게 관리해야 합니다.
본 글은 실제 프로덕션에서 Zebra 라벨 프린터를 통합하며 내린 결정—클래스 기반 싱글톤 서비스—의 이유와 구현을 원문 코드 중심으로 정리한 내용입니다.


문제 정의와 선택 배경

Zebra 라벨 프린터 통합은 하드웨어라는 단일·희소 자원을 다룹니다. 동시에 웹 앱(특히 React)에서는 화면 전환/재렌더링, 비동기 초기화, 중복 호출이 일어나기 쉽죠.
다음 요구사항이 핵심이었습니다:

  • 싱글 연결: 물리 프린터는 사실상 단일 연결/세션 가정이 안전
  • 상태 일관성: 앱 전역에서 동일한 프린터/연결 상태 공유
  • 초기화 중복 방지: 비동기 초기화 경쟁 조건(Race Condition) 제거
  • 명확한 라이프사이클: 초기화 → 사용(출력) → 정리의 수명 주기 캡슐화

따라서 클래스 기반 싱글톤 서비스가 자연스러운 해법.


함수 vs 클래스

함수형 예시

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// ❌ 문제가 될 수 있는 함수형 접근
let browserPrint: ZebraBrowserPrintWrapper | null = null; // 전역 변수
let isInitialized = false;
let selectedPrinter: ZebraDevice | null = null;

export const createPrinterService = () => {
return {
initialize: async () => {
/* ... */
},
printLabel: async () => {
/* ... */
},
cleanup: () => {
/* ... */
},
};
};

// 문제점들:
const service1 = createPrinterService(); // 독립적 인스턴스
const service2 = createPrinterService(); // 또 다른 독립적 인스턴스
// → 하드웨어 리소스 충돌 가능성

단점: 함수형으로는 인스턴스 통제와 프라이빗 상태 보호가 약하고, 전역 모듈 상태는 캡슐화에 불리합니다.


클래스 기반 예시

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
export class ZebraLabelService {
// ✅ 캡슐화된 상태
private static instance: ZebraLabelService;
private browserPrint: ZebraBrowserPrintWrapper | null = null;

// ✅ 인스턴스 제어
private constructor() {}
static getInstance(): ZebraLabelService {
/* ... */
}

// ✅ 타입 안전성
async ensurePrinterReady(): Promise<void> {
/* ... */
}
}

장점:

  • 상태 캡슐화 & 은닉
  • 싱글톤으로 유일 인스턴스 보장
  • this 컨텍스트로 상태 일관 접근
  • 타입 안전성 확보

왜 싱글톤인가?
1. 하드웨어 리소스 충돌 방지, 2) 전역 상태 일관성, 3) 메모리/초기화 비용 절감, 4) 초기화 재사용.


서비스 레이어 아키텍처와 의존성 관리

프레젠테이션(UI)과 비즈니스 로직을 분리한 Service Layer 패턴을 채택합니다.

1
2
3
4
5
// Service Layer (Business Logic)
ZebraLabelService.getInstance().printSeparateLabels(asset);

// Presentation Layer (React Hook)
const {printSeparateLabels, isInitialized} = useAssetLabelOutput();

의존성 생성 지점을 초기화 시점으로 미루는 형태:

1
2
3
4
5
// 외부 의존성을 생성자가 아닌 메서드에서 주입
async initialize(options: PrinterInitOptions = {}) {
this.browserPrint = new ZebraBrowserPrintWrapper();
// ...
}
  • 테스트 시 Mock 주입 용이
  • 교체 가능성 확보

단계별 실용 가이드

1) 중복 초기화 방지: Promise 캐싱

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
async initialize(options: PrinterInitOptions = {}): Promise<boolean> {
// 이미 초기화된 경우
if (this.isInitialized && this.selectedPrinter && this.browserPrint) {
return true;
}

// 초기화 진행 중인 경우 - Promise 재사용
if (this.initPromise) {
return this.initPromise;
}

// 새로운 초기화 시작
this.initPromise = this.performInitialization(options);
const result = await this.initPromise;
this.initPromise = null;

return result;
}
  • 효과: 동시 호출에서도 단 한 번만 초기화 수행
  • 장점: 리소스 절약, Race Condition 예방

2) 상태 기반 사전 검증: fail-fast

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

async ensurePrinterReady(): Promise<void> {
if (!this.isInitialized || !this.browserPrint) {
throw new Error('프린터가 초기화되지 않았습니다');
}

const status = await this.checkPrinterStatus();

if (!status.isConnected) {
throw new Error(`프린터가 연결되지 않았습니다: ${status.errorMessage}`);
}

if (!status.isReady) {
throw new Error('프린터가 준비되지 않았습니다. 프린터 상태를 확인하고 용지와 리본을 점검해주세요.');
}
}

  • 명확한 전제조건 확인
  • 친절한 에러 메시지로 문제 지점 즉시 파악

3) 수명 주기(Lifecycle) 명료화

1
2
3
4
5
6
7
8
9
// 초기화
async initialize(options: PrinterInitOptions = {}): Promise<boolean>

// 사용
async printSeparateLabels(asset: AssetLabelData): Promise<boolean>
async ensurePrinterReady(): Promise<void>

// 정리
cleanup(): void
  • 초기화 → 사용 → 정리의 경로가 코드 레벨에서 분명
  • UI/로직 레이어가 이 계약을 신뢰하고 사용 가능

성능/메모리 관점

구현 방식 인스턴스 수 메모리 사용량 초기화 비용
함수형 (팩토리) N개 N × 기본메모리 N × 비용
클래스형 (싱글톤) 1개 1 × 기본메모리 1 × 비용

싱글톤은 단 한 번 초기화로 비용/부하를 낮추고, GC 압박도 줄입니다.


React Hook과의 통합 & 에러 처리 일관성

Hook 통합 예시 (UI는 UI만, 서비스는 로직만)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// useAssetLabelOutput.ts에서의 사용
const printSeparateLabels = useCallback(
async (asset: AssetLabelData): Promise<boolean> => {
if (!isInitialized) {
await initializePrinter();
}

setIsPrinting(true);
try {
const service = ZebraLabelService.getInstance(); // 싱글톤 접근
return await service.printSeparateLabels(asset);
} finally {
setIsPrinting(false);
}
},
[isInitialized, initializePrinter, modals],
);
  • 책임 분리가 뚜렷: Hook은 상태/인터랙션, Service는 비즈니스 로직

에러 처리 표준화

1
2
3
4
5
6
7
8
9
// 모든 메서드에서 일관된 에러 처리
try {
await this.ensurePrinterReady();
// 실제 작업 수행
} catch (error) {
const printError = createPrintError(error, {tagNo, assetId});
logServerError(printError);
throw printError;
}
  • 로그 포맷/전송 일관성
  • 상위(UI)로 의미 있는 예외만 전달

의사결정 매트릭스 & 권장 사항

클래스 기반 싱글톤을 권장하는 상황

  • 단일 하드웨어 또는 외부 리소스와 연결(프린터, 스캐너, 시리얼 장치 등)
  • 초기화 비용이 크고 재사용 가치가 높은 경우
  • 앱 전역에서 동일한 상태/연결을 공유해야 할 때
  • Race Condition과 중복 초기화를 반드시 피해야 할 때

주의/한계
전역 싱글톤 남용은 테스트 격리/병렬성 저하를 유발 가능
→ 본 설계는 테스트에서 Mock 주입 경로(초기화 시점 의존성 생성)로 이 문제를 최소화


실전 체크리스트

  • 초기화는 반드시 한 번만: initPromise로 동시 호출 수렴
  • 모든 퍼블릭 메서드 전에 ensurePrinterReady 선행 검증
  • Hook에서는 UI 상태(로딩/에러/완료)만 관리
  • 에러 포맷/로깅 일관성 유지
  • cleanup() 경로 마련: 페이지 이탈/앱 종료 시 안전 해제
  • 테스트에서 Mock 가능한 경계(래퍼/드라이버 주입 지점) 확보

결론

클래스 기반 싱글톤 ZebraLabelService는 하드웨어 통합의 현실적 제약(단일 연결, 비싼 초기화, 복잡한 상태/수명 주기)을 안전하게 캡슐화하고
React UI와는 느슨하게 결합해 유지보수성/재사용성/테스트 용이성을 모두 확보합니다.

서비스 레이어(비즈니스 로직) ↔ Hook 레이어(UI 상태) ↔ 컴포넌트 레이어(UI 렌더)의 관심사 분리가 명확해져 실무에서 바로 적용 가능한 구조를 제공합니다.

FECConf 2025 다녀와서, 드디어 블로그를 시작하기로 했다

발표가 끝나자마자 노트북을 꺼내 메모장을 열었다. “이제 정말 시작해야겠다”는 생각이 머리를 스쳤다.
2025년 8월 23일 토요일, 처음으로 찾은 FECConf 2025 현장은 에너지와 열정으로 가득했다. 발표를 듣는 순간보다도, 세션이 끝나고 스피커 혹은 리더분들과 이야기를 나누는 순간순간이 더 오래 남았다.

컨퍼런스에서 얻은 가장 큰 깨달음

FECConf는 항상 개발자로서 한 단계 성장할 수 있는 계기를 주지만, 이번에는 조금 달랐다. 좋은 경험을 가진 리더분들과 대화하면서 느낀 건 꾸준히 쌓아가는 습관이 결국 나를 만든다는 것이었다.
기술 스택이나 최신 트렌드보다도 매일 조금씩 기록하고 나누고 돌아보는 과정이 진짜 성장을 만든다는 점을 실감했다.

누군가는 “짧은 글이라도 꾸준히 쓰다 보면 어느 순간 내 글이 다른 사람에게 힘이 된다”고 했다. 그 말이 크게 와 닿았다. 나 역시 지난 몇 년간 다양한 경험을 쌓아왔지만, 그 경험을 흘려보내는 데 그치지 않고 기록으로 남겼다면 지금쯤은 더 단단한 기반을 만들 수 있었을 거다.

블로그를 시작하는 이유

그래서 나는 결심했다. 내 경험과 지식을 글로 정리해보자.

  • 그동안 프로젝트에서 겪은 시행착오
  • 새로운 기술을 도입하며 배운 점
  • 개발자로서 성장하며 느낀 고민들

이 모든 것들을 차근차근 풀어내려고 한다. 단순히 기술 정리 노트가 아니라 “내가 어떻게 성장해왔는가””를 보여주는 기록으로 남기고 싶다.

특히 나는 오랜 시간 다양한 분야를 경험해왔다. 프론트엔드, 인프라, CI/CD, 모바일 앱까지… 이 모든 걸 그냥 머릿속에만 두는 건 아깝다는 생각이 들었다.
그래서 앞으로는 최소한 한 달에 2편 이상의 글을 꾸준히 올리는 것을 목표로 삼으려 한다.

다짐

꾸준함은 늘 어렵다. 하지만 이번에는 컨퍼런스에서 만난 분들이 보여준 태도와 습관을 본받아, 기록하는 습관을 개발자로서의 생활에 꼭 녹여내고 싶다.
글을 쓰는 과정에서 나 자신을 돌아보고, 또 누군가에게는 도움이 될 수 있다면 더할 나위 없을 것이다.

“성장은 순간이 아니라 과정이다.”
이번 블로그는 그 과정을 담는 첫 걸음이 될 것이다.

마무리

혹시 이 글을 읽는 분들 중에서도 비슷한 고민을 하고 있다면, 함께 시작해보면 어떨까? 완벽할 필요도 길게 쓸 필요도 없는 것 같다. 중요한 건 멈추지 않는 꾸준함이라는 걸 이번 컨퍼런스에서 확실히 배웠다.

앞으로 차근차근 글을 쌓아가며 함께 성장해나갔으면 좋겠다. 여러분의 경험과 의견도 댓글로 나눠주시면 정말 큰 힘이 될 것 같다.

[JS] ES2022 신기능 중 4가지

[JS] ES2022 신기능 중 4가지

이채현

노션에서 보기

Top level await

기존 await를 사용하려면 async 함수 내에서만 가능했다.

1
2
3
(async function () {
await foo();
})();

하지만, ES2022부터 이러한 규칙이 사라지고, awaitasync함수없이 모듈에서 아래와 같이 작성가능해졌다.

1
await foo();

Error Cause

이 기능을 통해 오류의 원인을 설명하여, 더 나은 오류 메시지를 만들 수 있다.

기존 오류를 만들 때는 오류 메시지를 작성하는 것 밖에 할 수 없었다.

1
new Error("This is the Error Message!");

하지만 ES2022부터 무엇이 오류를 발생시켰는지에 대해 설명할 수 있다. 그래서 정확히 무엇이 잘못되었는지 구체적으로 알 수 있으며, 동일한 오류메시지를 사용하지만 다른 원인을 작성할 수 있다.

또한, .cause를 액세스할 수 있다.

1
2
3
4
const err = new Error("This is the Error Message!", { cause: "Test Message" });

err.message; // This is the Error Message!
err.cause; // Test Message

.at()

.at을 활용하면, 배열의 모든 인덱스에 액세스할 수 있다.

1
2
3
const number = ["1", "2", "3", "4", "5"];

number.at(2); // 2

이전에도 대괄호를 사용하여 액세스할 수 있었지만, 대괄호와의 차이점은 뒤로 검색가능하다는 점이다. 대괄호는 -1 로 검색할 수 없었지만, .at을 통하면 가능하다.

1
2
3
4
5
const number = ["1", "2", "3", "4", "5"];

number.at(2); // 2
nubmer.at(-1); // 5
~~number[-1]; // 불가능~~

Class Fields

이것을 통해, 자바스크립트가 조금 더 OOP스럽게 느껴지게 한다. 이전에는 불가능했던 Private 메서드 및 속성을 가질 수 있으며, static 메서드를 사용할 수 있다.

그리고 속성을 초기화하기 위해 constructor를 사용할 필요도 없어진다.

Private 메서드 및 속성

private 메서드나 속성을 만들려면 이름 앞에 # 기호를 사용하면 된다.

1
2
3
4
5
6
7
8
9
10
11
12
class Message {
#destruct() {
console.log("booom!!");
}
}

const but = new Message();
~~btn.#destruct(); // 작동하지 않는다~~

class Message {
#text = "Hi";
}

위 코드처럼 private한 메서드 및 텍스트를 만들 수 있으며, constructor를 사용하지 않는다.

1
2
3
4
5
class Message {
constructor() {
this.text = "Hi";
}
}

Static 메서드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Message {
// body
}

Message.build(){
// body
}

====

class Message {
static build() {
// body
}
}

출처

방금 출시된 ⚡️자바스크립트 미친 신기능 4개!

[JS] ESLint 알고쓰기 : 설정 설명

[JS] ESLint 알고쓰기 : 설정 설명

이채현

노션 링크

이 문서는 eslint.org를 참고하여 eslint 7.32.0 버전에서 작성되었으며, 아래 프로젝트를 기반으로 작성했습니다.

https://github.com/chlee1001/react-typescript-simple-boilerplate

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
{
"env": {
"browser": true,
"es2021": true,
"node": true
},
"extends": [
"airbnb",
"airbnb-typescript",
"airbnb/hooks",
"plugin:@typescript-eslint/recommended",
"plugin:@typescript-eslint/recommended-requiring-type-checking",
"plugin:import/recommended",
"plugin:import/typescript",
"plugin:jsx-a11y/recommended",
"plugin:prettier/recommended",
"plugin:react/recommended",
"plugin:react-hooks/recommended"
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"project": [
"./tsconfig.json"
],
"ecmaFeatures": {
"jsx": true
},
"ecmaVersion": "latest",
"sourceType": "module"
},
"plugins": [
"react",
"@typescript-eslint",
"import"
],
"rules": {
"import/prefer-default-export": "off",
"import/no-unresolved": 0,
"import/extensions": [
"off"
],
"react/function-component-definition": [
2,
{
"namedComponents": "arrow-function",
"unnamedComponents": "arrow-function"
}
]
},
"ignorePatterns": [
"paths.js",
"webpack.*.js",
"dist/*",
"node_modules/*"
],
"settings": {
"react": {
"version": "detect"
}
}
}

ENV

  • browser - browser global variables : true를 하게되면 **console.log()**를 에러없이 사용할 수 있다.
  • node - Node.js global variables and Node.js scoping : true를 하게되면 전역에서 require를 에러없이 사용할 수 있게된다.
  • es2021 - adds all ECMAScript 2021 globals and automatically sets the ecmaVersion parser option to 12.

EXTENDS

extends는 추가한 플러그인에서 사용할 규칙을 설정한다. 플러그인을 설치하여도, 플러그인은 일련의 규칙집합이며 플러그인을 추가하여도 규칙은 적용되지 않는다. 규칙을 적용하기 위해서는 추가한 플러그인 중, 사용할 규칙을 extends 내에 추가해야한다. 보통 대부분의 플러그인은 recommendedstrict, all 등의 자체 설정을 제공한다.

  • recommended: 프로젝트에 권장하는 규칙 집합
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
...
"extends": [
"airbnb", // airbnb의 규칙 사용
"airbnb-typescript", // TS를 지원하는 airbnb 규칙 구성 향상
"airbnb/hooks", // React hooks를 위한 airbnb 규칙사용
"plugin:@typescript-eslint/recommended", // ESLint가 TypeScript를 지원할 수 있도록 하는 모든 도구를 위한 Monorepo
"plugin:@typescript-eslint/recommended-requiring-type-checking", // 일부 highly valuable rules를 올바르게 구현하기 위해 유형 검사를 위한 추천 요구 유형 검사
"plugin:import/recommended", // import명이나 잘못 작성한 파일 경로에 대한 이슈를 방지해주는 플러그인 (아래 errors와 warnings의 집합)
// "plugin:import/errors",
// "plugin:import/warnings",
"plugin:import/typescript", // TS에서 `eslint-plugin-import`를 사용하기 위해 추가
"plugin:jsx-a11y/recommended", // JSX 요소에 대한 접근성 규칙
"plugin:prettier/recommended", // prettier 규칙을 적용하여 틀릴 경우 eslint 문제로 처리
"plugin:react/recommended", // eslint-plugin-react의 추천 규칙 사용
"plugin:react-hooks/recommended"
],
...

PARSER

코드를 분석하기 위한 파싱툴이다. 기본값은 espress이고, 보통 js 워크스페이스에서는 @babel/eslint-parser를 사용하고 ts 워크스페이스인 경우 @typescript-eslint/parser를 사용한다. 사실 plugin:@typescript-eslint/recommended를 포함시키면 @typescript-eslint/parser가 자동으로 포함되기도 한다.

1
"parser": "@typescript-eslint/parser" 

PARSER OPTIONS

parserOptions은 ESLint 사용을 위해 지원하려는 Javascript 언어 옵션을 지정할 수 있습니다.

  • project: 이 옵션을 사용하면 제공된 tsconfig에서 정의한 프로젝트에 포함되지 않은 파일이 허용되도록 요청할 수 있다.
  • ecmaVersion: 사용할 ECMAScript 버전을 설정
  • sourceType: parser의 export 형태를 설정
  • ecmaFeatures: ECMAScript의 언어 확장 기능을 설정
    • globalReturn: 전역 스코프의 사용 여부 (node, commonjs 환경에서 최상위 스코프는 module)
    • impliedStric: strict mode 사용 여부
    • jsx: ECMScript 규격의 JSX 사용 여부
1
2
3
4
5
6
7
8
9
10
"parserOptions": {
"project": [
"./tsconfig.json"
],
"ecmaFeatures": {
"jsx": true
},
"ecmaVersion": "latest",
"sourceType": "module"
},

PLUGIN

플러그인 패키지를 설치하고, 해당 플러그인을 plugins에 추가하여 사용할 수 있다.

1
2
3
4
5
"plugins": [
"react",
"@typescript-eslint",
"import"
],

RULES

ESLint에는 프로젝트에서 사용하는 규칙을 수정할 수 있다. 규칙을 변경하는 경우, 다음과 같은 방법으로 설정해야한다.

  • "off" 또는 0: 규칙을 사용하지 않음
  • "warn" 또는 1: 규칙을 경고로 사용
  • "error” 또는 2: 규칙을 오류로 사용

규칙에 추가 옵션이 있는 경우에는 배열 리터럴 구문을 사용하여 지정할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
"rules": {
"import/prefer-default-export": "off",
"import/no-unresolved": 0,
"import/extensions": [
"off"
],
"react/function-component-definition": [
2,
{
"namedComponents": "arrow-function",
"unnamedComponents": "arrow-function"
}
]
}

규칙 무시하기

파일 디렉토리 제외

ignorePatterns 필드 또는 .eslintignore 파일을 작성하여 파일 및 디렉토리를 제외하도록 지정할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
// .eslintrc 파일 ignorePatterns 설정

"ignorePatterns": [
"paths.js",
"webpack.*.js",
"dist/*",
"node_modules/*"
],

//.eslintignore 파일 생성
config/
dist/
node_modules/

대체파일 사용

.eslintignore를 현재 작업 디렉토리가 아닌 다른 파일을 사용하려면 --ignore-path 옵션을 사용하여 명령행에 파일을 지정할 수 있다.

1
$ eslint --ignore-path .gitignore file.js

인라인으로 규칙 비활성화

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
**// 전체 파일 규칙 경고 비활성화, 파일 맨위에 아래 블록 주석 추가**
/* eslint-disable */
...
...
/* eslint-enable no-alert, no-console */

**// 경고 비활성화 블록 주석**
/* eslint-disable */
alert('foo');
/* eslint-enable */

**// 특정 규칙 경고 비활성화**
/* eslint-disable no-alert, no-console */
alert('foo');
console.log('bar');

**// 다음 한줄 경고 비활성화**
// eslint-disable-next-line

참고

Configuring ESLint - ESLint - Pluggable JavaScript Linter

https://velog.io/@kyusung/eslint-config-2

ESLint 알고 쓰기

[JS] 선언한 모듈로 이동하기 (alias) - Go to declaration

[JS] 선언한 모듈로 이동하기 (alias) - Go to declaration

이채현

문제

많은 사람들은 선언한 모듈들을 command/ctrl + click으로 해당 파일로 바로 이동하거나 자동완성이 되게하는 IDE나 Editor의 기능을 사용할 것이다. 그리고 babel-plugin-module-resolver을 통해 모듈의 경로를 별칭으로 바꿔서 사용할 것이다. 하지만 별칭으로 바꾸면서 위 기능이 깨지는 문제가 종종 있다. 그리고 이 문제는 플러그인쪽에서는 해결되지 않고 있다. npm에 올라온 최신버전은 이미 2년이 지났다.

babel-plugin-module-resolver

해결

우리는 jsconfig.json을 사용하여 IDE가 사용자 지정 resolve규칙을 따르도록 하는 것이 좋다. 이 접근 방식은 WebstormVS Code 모두 작동한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"#api/*": [
"./src/api/*"
],
"#assets/*": [
"./src/assets/*"
],
"#common/*": [
"./src/common/*"
],
"#component/*": [
"./src/component/*"
],
"#constant/*": [
"./src/constant/*"
],
"#container/*": [
"./src/container/*"
]
}
}
}
[JS] babelrc와 webpack.config

[JS] babelrc와 webpack.config

이채현

Webpack으로 React 프로젝트를 초기 설정하다가 ,

1
2
3
4
5
6
7
8
9
10
11
12
presets: [
[
'@babel/preset-env',
{
targets: {
browsers: ['> 5% in KR', 'last 2 chrome versions'],
},
debug: true,
},
],
'@babel/preset-react',
],

위 코드의 presets가 과연 .babelrc에 있어야 하는지, webpack.config.js에 있어야하는지 잘 모르겠어서 각 파일의 목적을 정리해보았다.

babelrc

.babelrcbabel의 설정을 위해 사용한다.

1
2
3
4
{
"presets": [...],
"plugins": [...]
}

webpack.config

물론 webpack.config.jswebpack의 설정을 위해 사용한다. 프로젝트 파일의 번들링과 관련된 설정들을 작성해준다. 그리고 babel과 관련된 설정들을 .babelrc가 아닌 webpack.config.js에서 babel-loader를 설정한 부분에 작성해줄 수도 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
...

module.exports = {

...

module: {
rules: [
{
test: /\\.js?/,
loader: 'babel-loader',
options: {
presets: [
[
'@babel/preset-env',
{
targets: {
browsers: ['> 5% in KR', 'last 2 chrome versions'],
},
debug:true,
},
],
'@babel/preset-react',
],
plugins: [
'@babel/plugin-proposal-class-properties',
'react-refresh/babel',
[
'module-resolver',
{
root: ['./src'],
alias: {
'#api': './src/api/',
'#assets': './src/asset/',
'#common': './src/common/',
'#component': './src/component/',
'#constant': './src/constant/',
'#container': './src/container/',
},
},
],
],
exclude: /node_modules/,
},
},
],
},

...

}

결론

결론은, babelpresetswebpack.config.js.babelrc 파일 둘 중 한 곳에만 있으면 된다! 그러나 babel cli를 이용하여 직접 코드 변환을 수행하거나 babel test 등을 돌릴 때에는 webpack을 거치지 않기 때문에 .babelrc에 작성하는 방식이 권장된다.

나는 webpack.config.js 내에 적어서 사용한다.

[JS] Babel로 별칭 경로 설정하기

[JS] Babel로 별칭 경로 설정하기

이채현

프로젝트의 규모가 커지면 디렉토리 구조도 복잡해진다. 그 때문에 아래와 같이 컴포넌트의 위치를 찾기 어려워진다.

1
import { whereIsThis } from "../../../../../aaa/bbb/ccc";

위와 같이 작성한 것을 상대경로라고 한다. 상대 경로를 사용해서 모듈을 불러오면 모듈이 어느 경로에 위치하는지 파악하기가 난해해지는 경우가 생긴다. 뿐만 아니라, 이 자바스크립트 파일을 다른 디렉토리로 옮기려면 상대 경로를 그에 따라 모두 수정해줘야 해서 코드 리펙토링(refactoring)이 상당히 불편하다.

물론 절대경로를 사용하면 되지 않을까 생각할 수 있지만, 개발자들마다 해당 프로젝트를 다른 디렉토리에 저장해놓을 것이기 때문에 현실적으로 적용하기 어려운 방법이다.

별칭 경로

위와 같은 문제는 자바스크립트 트랜스파일(transpile) 도구인 Babel(바벨)을 사용하면 이 문제를 비교적 간단하게 해결할 수 있습니다.

Babel의 플러그인을 사용해서 별칭(alias) 경로를 설정해주면 된다.

1
$ yarn add -D babel-plugin-module-resolver

Babel의 module resolver 플러그인을 개발 의존성으로 설치 후, .babelrc설정 파일을 열고, plugins항목에 module-resolver설정을 추가해준다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
presets: ['@babel/preset-env', ...],
plugins: [
...
[
'module-resolver',
{
root: ['./src'],
alias: {
'#api': './src/api/',
'#assets': './src/asset/',
'#common': './src/common/',
'#component': './src/component/',
'#constant': './src/constant/',
'#container': './src/container/',
},
},
],
...
],

alias 부분에서 ./src/api#api로 표기함에 따라

1
2
3
4
5
import { alias } from "./src/api/.../alias";

// 위 코드를 아래와 같이 작성할 수 있습니다.

import { alias } from "#api/alias";

마치면서

이상으로 Babel의 module resolver 플러그인을 이용한 별칭 경로 설정 방법이였습니다.

만약 본인이 Webpack 설정과 겹쳐 고민이 있다면 다음 글[JS] babelrc와 webpack.config을 참고해주세요.

[CSS] 하드웨어 가속을 적극 활용하자 (translate3d)

[CSS] 하드웨어 가속을 적극 활용하자 (translate3d)

이채현

하드웨어 가속은 왜 필요할까?

복잡한 계산이 필요한 인터렉션은 렌더링에 많은 시간이 걸리게 된다. 이런 부분을 CPU가 아닌 GPU가 계산하도록 만들면 렌더링 시간을 줄일 수 있게 된다.

*하드웨어 가속을 사용하려면 어떤 스타일을 사용해야 할까?*

대표적으로 animation(keyframes), will-change 속성이 있지만 두 속성은 사용하는 데 있어서 한계가 있다.

다양한 케이스에 대응하기 쉽고 초기 렌더링에도 크게 영향을 주지 않는 방법으로는 transform: translate3d 스타일을 사용하면 된다. will-change, translate3d속성은 브라우저에게 **"얘는 3D 요소니까 하드웨어 가속을 써야 해!"**라고 알려주며 대상이 되는 요소를 자체 레이어로 승격시키고 GPU 메모리에 할당이 되어 하드웨어가 계산을 하게 된다.

translate3d

translate3d()는 x, y, z 3차원의 값을 조정할 수 있다.

translate3d의 동작원리는 translate와 동일하다.

Y축으로 100% 원한다면 아래와 같이 작성가능하다.

1
2
3
4
5
6
7
.anim {
transform: translate3d(0, 100%, 0 );
}

.anim2 {
transform: translateY(100%);
}

translate3d vs translateX, translateY

translate3d는 하드웨어 가속, 즉 GPU를 사용하기 때문에 CSS 퍼포먼스가 일반적인 translate()보다 더 좋다. 따라서 더 좋은 퍼포먼스를 원한다면 translate3d를 사용하는 것이 좋을 듯 싶다.

하드웨어 가속 사용 시 고려 사항

하드웨어 가속을 사용하면 웹 페이지의 렌더링 속도가 빨라지지만 잘못 사용하면 오히려 렌더링 속도가 느려지거나 브라우저에 문제가 일어날 수 있다.

주의 사항

하드웨어 가속을 사용하면 다양한 성능 향상을 기대할 수 있지만, 그렇다고 모든 요소를 대상으로 적용하면 안 된다. 하드웨어 가속 대상을 지정할 때 다음의 사항을 기억하기 바란다.

  • 무분별한 하드웨어 가속은 오히려 브라우저를 느리게 한다.
  • 요소에 하드웨어 가속 속성이 부여되면 즉시 대상 영역이 GPU에 업로드되며, 이때 업로드되는 영역이 크면 화면이 깜빡이는 현상이 발생될 수 있다.
  • 요소에 하드웨어 가속 속성이 부여되면 레이어로 분리되며, 레이어는 변경되는 내용이 없는 한 요소를 GPU 메모리에 다시 업로드하지 않는다.
  • 하드웨어 가속 속성을 사용한 요소의 내용이 변경되면 GPU 메모리가 갱신되므로 요소의 내용을 미리 변경한 다음 하드웨어 가속 속성을 부여한다.
  • 성능이 낮은 기기에서 하드웨어 가속을 사용하면 오히려 성능 저하를 가져올 수 있다.

적용 시 고려 사항

하드웨어 가속을 사용할 때는 다음과 같은 점을 고려한다.

  1. 하드웨어 가속을 적용하는 요소의 크기는 작을수록 좋고, 요소의 개수는 화면에서 5~6개로 구성하는 것이 좋다.특히, 요소의 속성값에 따라 요소의 영역이 커질 수 있기 때문에 주의해서 적용해야 한다. 예를 들어 text-indentleft 같은 속성에 999em이나 9999px과 같이 화면 영역을 지나치게 벗어나게 값을 설정하면, 콘텐츠 영역의 크기가 늘어나고 하드웨어 가속에 의해 구성된 레이어도 커지게 돼 불필요한 메모리를 사용하게 된다.
  2. DOM 요소의 내용이 자주 변경되지 않는 영역에 하드웨어 가속을 적용한다.내용 변경이 아닌 이동이나 크기 변경이 자주 발생하는 영역에 하드웨어 가속을 적용하고, 이동이나 크기 변경은 transform 속성을 사용한다.
  3. 기기에 따라 선별적으로 하드웨어 가속을 적용한다.JMC(Jindo Mobile Component)는 기기가 하드웨어 가속에 적합한 기기인지 확인할 수 있게 useCss3d() 메서드를 제공한다.

참고 자료:

카카오웹툰은 하드웨어 가속과 IntersectionObserver를 어떻게 사용했을까?

NAVER D2

[React] React의 setState는 비동기로 동작한다

[React] React의 setState는 비동기로 동작한다

이채현

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function App() {
const [state, setState] = useState(0);

const handleClick = () => {
setState(state + 1);
setState(state + 1);
setState(state + 1);
};

return (
<div className="App">
<p>{state}</p>
<button
onClick={() => {
handleClick();
}}
>
+3
</button>
</div>
);
}

export default App;

위 코드의 결과는 버튼을 눌러도 1씩 증가한다.

이유는?

결국, 위 코드는 setState 는 동일한 key 값에 대하여 이전의 값을 계속해서 덮어써서, 마지막 명령어만 수행되는 셈이다.

setState를 동기적으로 처리하려면!

setState의 인자로 함수를 집어넣는 것이다.

1
2
3
4
5
6
7
...
const handleClick = () => {
setState((prevState) => prevState + 1);
setState((prevState) => prevState + 1);
setState((prevState) => prevState + 1);
};
...

위와 같이 작성하면 setState 내 함수의 매개변수로 이전 상태 값이 들어오므로, 동기적으로 동작하는 것을 확인할 수 있다. 이제 한번의 버튼 클릭으로 카운트가 3씩 증가하는 것을 볼 수 있다.

결론