왜 클래스 기반 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 렌더)의 관심사 분리가 명확해져 실무에서 바로 적용 가능한 구조를 제공합니다.

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

https://devch.co.kr/categories/웹앱/React/react-why_use_class-1-250827/

Author

Chaehyeon Lee

Posted on

2025-08-27

Updated on

2025-08-28

Licensed under

댓글