728x90

며칠 전 GeekNews 피드를 넘기다가 "TypeScript를 JavaScript 엔진 없이 네이티브 바이너리로 컴파일한다"는 제목을 보고 잠깐 멈췄다. 인프라 쪽에서 일하다 보면 Node 컨테이너 이미지가 수백 MB로 부풀고, Lambda Cold Start가 몇백 ms씩 튀는 걸 매번 마주치는데, "엔진을 아예 빼버린다"는 문장은 그 지점을 정확히 건드린다.

Vercel Labs에서 공개한 scriptc 이야기다. 아직 공개된 지 며칠 안 된 실험 프로젝트라 프로덕션에 바로 넣을 물건은 아니지만, 아키텍처를 뜯어보면 우리가 매일 씨름하는 문제들과 어떻게 연결되는지 감이 온다. 실무자 관점에서 하나씩 정리해봤다.

1. 왜 지금 화제인가 — 어떤 문제를 푸는가

우리가 지금 TypeScript 코드를 서버에서 돌리는 방식을 생각해보자. 대략 이렇다.

  1. tsc 또는 esbuild/swc로 TS → JS 트랜스파일
  2. Node(내부적으로 V8) 위에서 JS 실행
  3. 컨테이너에는 Node 런타임 + node_modules가 통째로 들어감

이 구조의 근본적인 비용이 두 가지다. 첫째, 런타임이 무겁다. V8은 JIT 컴파일러와 GC를 포함한 거대한 엔진이고, 이걸 부팅하는 데 시간이 든다. 둘째, 배포 산출물이 크다. Node 베이스 이미지에 의존성까지 얹으면 이미지가 쉽게 수백 MB로 간다.

scriptc의 발상은 단순하다. 정적으로 분석 가능한 TypeScript 코드는 아예 네이티브 코드로 컴파일해버리고, JS 엔진을 바이너리에 넣지 않는다. 원문 발췌 기준 수치를 그대로 옮기면 이렇다.

  • 시작 시간: 약 2.4ms (Node는 약 47ms)
  • 정적 바이너리 크기: 170~200KB (Node SEA는 60~100MB)
  • 일반 RSS(메모리): 1~4MB (Node는 67~116MB)

측정 환경은 Apple M 시리즈이고, Node·Go·Rust·Zig와 동일 작업·동일 출력 기준으로 비교했다고 명시돼 있다. 숫자만 보면 "이게 진짜면 CLI 도구 배포 판이 바뀌겠는데" 싶은 수준이다. 다만 이 수치들은 벤더 자체 측정이니, 실제 워크로드에서 재현되는지는 각자 검증해야 한다.

2. 핵심 동작 원리 — 세 갈래로 갈리는 컴파일

scriptc를 이해하는 핵심은 "모든 코드를 다 네이티브로 만들지는 않는다"는 점이다. 코드를 세 부류로 나눠서 처리한다.

2-1. 정적 컴파일 (기본 모드)

타입 정보로 구조를 확정할 수 있는 코드는 네이티브로 컴파일한다. 파이프라인은 이렇게 흐른다.

TypeScript
  → tsc 파싱 · 타입 검사
  → lowering
  → typed IR
  → C
  → clang
  → 네이티브 실행 파일

여기서 중요한 건 프런트엔드가 실제 TypeScript 컴파일러(tsc) API를 쓴다는 점이다. 별도 방언이나 어노테이션이 아니라, 우리가 쓰던 그 tsconfig.json의 strict 설정과 es2025 라이브러리를 그대로 적용한다. @types/node가 프로젝트에 있으면 그것도 같이 타입 검사한다. 즉 "타입 검사는 진짜 tsc, 코드 생성은 네이티브"라는 조합이다.

비유하자면 판별 유니온(discriminated union) 같은 걸 생각하면 된다. TypeScript에서 태그 필드로 narrowing 하던 패턴을, scriptc는 그 태그 값을 그대로 활용해 네이티브 분기로 바꾼다. 동적 디스패치도 안전성이 증명되면 비가상화(devirtualization)한다. 백엔드는 LLVM이 기본이고, C 백엔드도 참조 구현으로 유지돼서 --backend c로 사람이 읽을 수 있는 C 코드를 뽑아볼 수 있다.

2-2. 동적 실행 (--dynamic)

npm 패키지의 JavaScript나 any 타입처럼 정적으로 못 잡는 코드는 어떻게 하나. 여기서 --dynamic 플래그를 주면 약 620KB짜리 quickjs-ng 엔진을 번들에 넣어서 그 부분만 인터프리터로 돌린다.

포인트는 "정적 코드로 값이 넘어올 때 런타임 검증을 한다"는 것이다. 선언된 타입과 실제 값이 다르면 메모리를 깨뜨리지 않고 TypeError를 던진다. 이 경계 설계가 없으면 동적 영역에서 넘어온 이상한 값이 네이티브 메모리를 손상시킬 수 있으니, 안전장치로서 필수다.

2-3. 거부

세 번째는 그냥 거부다. 처리 못 하는 구문은 조용히 잘못 컴파일하지 않고, 오류 코드·코드 프레임·수정 힌트를 주며 컴파일을 중단한다. 개인적으로 이 설계 방향은 마음에 든다. "어중간하게 돌아가는 것"보다 "명확하게 안 되는 것"이 인프라 입장에서는 훨씬 다루기 쉽다.

내 코드가 얼마나 정적 컴파일되는지 궁금하면 coverage 명령으로 확인할 수 있다.

$ scriptc coverage src/index.ts

# 예상 출력 형태 (원문 예시 기준)
Analyzed 4481 statements
Statically compiled: 4451 (99%)
Blocked: 30
  - dynamic npm import (E....)
  - any-typed value flow (E....)

원문 예시에서는 4,481개 문장 중 4,451개(99%)가 정적으로 컴파일됐다고 나온다. 다만 이건 "잘 맞는 코드베이스" 사례이고, 뒤에서 얘기할 함정을 보면 알겠지만 남의 라이브러리를 많이 끌어다 쓰면 이 숫자가 확 떨어진다.

3. 실무 관점 — 이점, 트레이드오프, 흔한 함정

3-1. 인프라 입장에서 매력적인 지점

이건 명확하다. 세 가지가 바로 와닿는다.

  • 컨테이너 이미지 경량화: 런타임 의존성 없는 단일 바이너리 170~200KB면, scratchdistroless 베이스에 바이너리 하나만 얹으면 끝이다. Node 이미지 수백 MB와 비교하면 레지스트리 저장 비용, 이미지 pull 시간, 배포 파이프라인 속도가 전부 달라진다.
  • Cold Start: Lambda나 Cloud Run에서 매번 걸리는 그 문제. 시작 2.4ms 수치가 실제로 재현된다면 Cold Start 논의 자체가 거의 사라진다.
  • 배포 단순화: node_modules, npm ci, lockfile 검증 같은 배포 단계 상당 부분이 빌드 시점으로 흡수된다. 실행 시점에는 node_modules를 읽지 않는다(동적 모드에서도 JS가 빌드 시 바이너리에 포함됨).

distroless 예시로 감을 잡아보면 이런 그림이다.

# 빌드 스테이지에서 scriptc로 네이티브 바이너리 생성
FROM node:22 AS build
RUN npm install -g scriptc
# clang 필요 (scriptc의 백엔드가 clang을 씀)
RUN apt-get update && apt-get install -y clang
WORKDIR /app
COPY . .
RUN scriptc build src/index.ts -o /app/server

# 런타임 스테이지 — 바이너리 하나만
FROM gcr.io/distroless/static
COPY --from=build /app/server /server
ENTRYPOINT ["/server"]

주의: 위 Dockerfile은 개념 예시다. Linux 바이너리는 교차 컴파일로 지원한다고 돼 있고, 주요 개발 플랫폼은 macOS arm64다. 리눅스 컨테이너 빌드에서 실제로 어떤 절차가 필요한지는 공식 문서 확인이 필요하다.

3-2. 트레이드오프 — 공짜가 아니다

먼저 속도. 시작은 빠르지만 처리 속도가 항상 빠른 건 아니다. HN 댓글에 나온 실측 사례를 그대로 옮기면, 바이트 배열 벤치마크에서 scriptc는 전용 최적화 후에도 Node 24보다 약 7.5배 느렸다고 한다. 대신 시작 12배 빠름(1.5ms vs 18.6ms), 메모리 72배 적게 씀(2.5MiB vs 181MiB)이었다. 즉 성격이 명확하다. 짧게 뜨고 죽는 워크로드(CLI, 함수형 서버리스)에 유리하고, 장시간 돌며 CPU를 갈아넣는 워크로드에는 불리하다. 이건 AOT 컴파일 언어 대비 JIT의 오래된 트레이드오프와 비슷하다.

다음으로 생태계. TypeScript의 진짜 힘은 언어 표현력이 아니라 npm 생태계 호환성이다. 그런데 대부분의 npm 패키지는 타입 선언(.d.ts)으로 인터페이스만 주고 실제 구현은 JS로 배포한다. 이걸 쓰려면 결국 --dynamic으로 quickjs를 끌어와야 하고, 그 순간 "네이티브의 이점"이 부분적으로 희석된다. 의존성이 많을수록 정적 비율이 떨어지고 바이너리가 커진다(동적 모드 + 내장 의존성 포함 시 약 3MB).

3-3. 흔한 함정 — 실제로 마주칠 에러

HN에서 여러 명이 자기 프로젝트에 돌려봤는데 "coverage 분석에서 수백 개 오류가 나서 사실상 못 썼다"는 후기가 있었다. 남의 라이브러리를 많이 쓰는 일반적인 앱을 그냥 넣으면 이 벽에 부딪힐 가능성이 높다.

특히 조심할 건 런타임 타입 단언이다. scriptc는 JSON.parse(...) as Config 같은 검사된 타입 단언에 런타임 검증 코드를 삽입한다. TypeScript에서 as는 컴파일 타임에 사라지는 캐스팅이라 우리가 "그냥 통과하겠지" 하고 넘기던 건데, scriptc에서는 실제 값이 선언과 다르면 이런 예외가 튀어나온다.

// config.ts
const config = JSON.parse(raw) as { port: number };

// 실행 시 raw가 { "port": "3000" } 이면:
Uncaught TypeError: expected number at $.port, got string

이건 사실 버그를 잡아주는 좋은 동작인데, 기존 Node에서는 조용히 통과하던 코드가 여기서는 죽는다는 걸 모르면 "왜 Node에선 되는데 여기선 터지지?" 하고 헤매게 된다. 원문에도 "Node와 의도적으로 다른 동작이 수십 건 있고 각각 번호가 부여돼 문서화된다"고 나온다. 마이그레이션할 때 이 차이 목록을 먼저 확인하는 게 순서다.

정적으로 지원 안 되는 구문을 만나면 컴파일 자체가 코드 프레임과 함께 거부된다(구체적 에러 코드 포맷은 문서 확인 필요). 요지는 런타임에 조용히 잘못되는 게 아니라 빌드에서 막힌다는 것이다.

3-4. 대안

목적별로 대안이 갈린다.

  • npm 생태계를 최대한 유지하며 빠른 런타임이 필요하다 → Bun, Deno가 현실적. 여전히 JS 엔진 기반이지만 성숙도와 호환성이 다르다.
  • 애초에 네이티브 컴파일이 목표이고 npm 의존이 거의 없다 → Go, Rust, Zig 같은 제대로 설계된 컴파일 언어를 쓰는 게 낫다는 지적이 HN에서 강하게 나왔다. TypeScript를 굳이 네이티브로 짜낼 이유가 약해진다.
  • 기존 큰 TypeScript 코드베이스와 코드를 공유하는 CLI 도구를 만든다 → 이 지점이 scriptc가 가장 설득력 있는 유스케이스다. 팀이 이미 TS로 통일돼 있고, 작고 빠르게 뜨는 도구가 필요할 때.

4. 정리 — 한 줄 요약과 사용 판단

한 줄 요약: scriptc는 "정적 분석 가능한 TypeScript를 JS 엔진 없이 네이티브 바이너리로 뽑고, 안 되는 부분만 quickjs로 폴백하는" 실험적 AOT 컴파일러다.

지금 시점(공개 며칠)에서 내 판단은 이렇다.

  • 당장 프로덕션 서버에 넣기: 아직 이르다. 생태계 호환성, 실사용 검증, 장기 유지보수 여부가 불확실하다. HN에서도 "5일 만에 전부 바이브 코딩된 프로젝트, 몇 달 뒤 유지보수 중단될 수도"라는 회의론이 상당하다.
  • 지금 시험해볼 만한 곳: 의존성 적은 CLI 도구, 사이드 프로젝트. 특히 이미 TypeScript로 굴러가는 팀에서 작고 빠른 내부 도구를 만들 때 scriptc coverage로 정적 비율을 재보는 것부터 시작하면 된다.
  • 주목할 이유: 방향성. GraalVM Native나 .NET AOT가 걸어온 길처럼, 이게 계속 발전하면 서버리스/컨테이너 배포 판을 흔들 잠재력은 있다. 다만 그 길이 길고 험하다는 것도 선례들이 보여준다.

결론적으로 나는 "재밌는 실험이고 방향은 맞지만, 지금은 벤치마크와 coverage로 감만 잡아두는 단계"로 본다. 각자 코드베이스에 coverage 한 번 돌려보고 정적 비율이 몇 %인지 보는 것만으로도, 우리 코드가 얼마나 "정적으로 설명 가능한가"를 되돌아보는 재미는 확실히 있다.

참고 자료

※ 본문의 수치(시작 2.4ms, 바이너리 170~200KB 등)는 Vercel 자체 측정 및 HN 댓글 실측 사례를 인용한 것으로, 실제 환경에서의 재현은 각자 검증이 필요하다. Dockerfile·명령어 예시는 개념 설명용이며 정확한 CLI 옵션과 플랫폼별 빌드 절차는 공식 문서 확인을 권한다.

728x90
728x90

어제 사내 Slack에 "TypeScript 7 정식 나왔다는데 이거 우리 CI 다시 봐야 하는 거 아니냐"는 메시지가 올라왔다. 프론트 팀이 아니라 인프라 팀에서 먼저 반응한 게 인상적이었다. 이유는 간단하다. TypeScript 7은 컴파일러 자체를 Go로 다시 짠 물건이라, 결국 빌드 시간CI 러너 비용에 직접 꽂히는 이야기이기 때문이다.

이 글은 원문(TypeScript v7 Released | GeekNews) 발표 내용을 기준으로, 빌드/CI를 운영하는 입장에서 뭘 확인하고 뭘 조심해야 하는지 정리한 것이다. 벤치마크 수치는 공식 발표 자료를 각자 확인하는 걸 전제로 하고, 이 글에서는 임의로 수치를 지어내지 않는다.

1. 도입: 왜 지금 화제이고 어떤 문제를 푸는지

기존 TypeScript 컴파일러(tsc)는 TypeScript/JavaScript로 작성돼 있었다. 즉 TS로 만든 컴파일러가 TS를 컴파일하는 구조였다. 이게 우아하긴 한데, 실무에서 프로젝트가 커지면 아픈 지점이 명확하게 나타난다.

  • 단일 스레드 JS 런타임 위에서 돌기 때문에, 대형 모노레포에서 tsc --build가 수십 초에서 분 단위로 늘어난다.
  • 에디터에서 타입 힌트가 뜨는 데 걸리는 지연(Language Server 응답)이 프로젝트 규모에 비례해 커진다.
  • CI에서 타입 체크 스텝이 전체 파이프라인 병목이 되는 경우가 흔하다.

TypeScript 7은 이 컴파일러를 Go로 재작성한 버전이다(프로젝트 코드네임은 흔히 tsgo로 불린다). Go를 택한 핵심 이유는 네이티브 컴파일과 동시성(goroutine 기반 병렬 처리)이다. JS 런타임의 단일 스레드 한계에서 벗어나 파싱·타입 체크 단계를 병렬화할 여지가 생긴다는 게 요지다.

주의할 점 하나. 원문 댓글에서도 지적됐듯이, 제대로 된 programmatic API는 7.0이 아니라 7.1에 공개될 예정이라고 한다. 즉 지금 시점에 "완제품 다 갈아끼우면 된다"가 아니라, API 표면이 아직 흔들리는 과도기라는 점을 전제로 봐야 한다.

2. 핵심: 동작 원리를 예시·비유로

비유하자면 기존 tsc는 "한 명의 숙련된 번역가가 문서를 처음부터 끝까지 혼자 번역하는" 방식이었다. Go 재작성 버전은 "번역가 여러 명이 문서를 챕터별로 나눠 동시에 번역하고, 마지막에 취합하는" 방식에 가깝다. 파일 파싱과 타입 검사처럼 병렬화가 가능한 구간을 코어 개수만큼 나눠 처리할 수 있게 된 것이다.

파이프라인 관점에서 컴파일러가 하는 일은 크게 세 단계다. 이건 기존과 개념적으로 동일하다.

  1. 파서(Parser): 소스 코드를 AST로 변환
  2. 타입 체커(Type Checker): AST를 순회하며 타입 검증 — 여기가 가장 무겁다
  3. 에밋(Emit): 검증된 결과를 JS로 출력(또는 --noEmit이면 생략)

Go 버전이 노리는 지점은 대부분 2번 타입 체커와 대형 프로젝트의 파일 로딩/파싱 병렬화다. 실제 설치는 별도 패키지로 배포되는 형태다. 실무에서 먼저 해볼 수 있는 건 기존 프로젝트를 건드리지 않고 병렬로 타입 체크만 돌려보는 것이다.

# 기존 프로젝트는 그대로 두고, Go 기반 컴파일러만 별도 설치해서 체크
npm install -D @typescript/native-preview

# 타입 체크만 실행 (에밋 없이)
npx tsgo --noEmit -p tsconfig.json

정상적으로 통과하면 출력은 조용하다. 타입 에러가 있으면 기존 tsc와 유사한 형태로 뜬다.

src/user/service.ts:42:18 - error TS2322: Type 'string | undefined' is not assignable to type 'string'.
  Type 'undefined' is not assignable to type 'string'.

42     const name: string = user.displayName;
                    ~~~~

Found 1 error in src/user/service.ts:42

여기서 확인할 포인트는 두 가지다. 첫째, 기존 tsc와 진단 결과(에러 개수/코드)가 일치하는가. 둘째, 실행 시간이 얼마나 줄어드는가. 시간 비교는 아주 간단하게 붙일 수 있다.

# 기존 tsc
time npx tsc --noEmit -p tsconfig.json

# Go 기반
time npx tsgo --noEmit -p tsconfig.json

수치는 프로젝트 규모와 러너 사양에 따라 천차만별이니, 반드시 본인 레포에서 직접 측정하길 권한다. 남의 벤치마크는 참고용일 뿐이다.

3. 실무 관점: 도입 시 고려사항, 트레이드오프, 흔한 함정

트레이드오프: 빠른 대신 아직 다 갖춰지지 않았다

속도는 매력적이지만, 현재 시점에서 무시할 수 없는 트레이드오프가 있다.

  • programmatic API 미완성: 원문에서 언급됐듯 완전한 API는 7.1 예정이다. ts.createProgram() 같은 API에 직접 의존하는 도구(커스텀 트랜스포머, 코드 생성기 등)는 지금 바로 갈아타기 어렵다.
  • 생태계 호환성: 많은 빌드 도구가 TS의 내부 API나 타입 정보에 의존한다. 대표적으로 아래 도구들은 확인이 필요하다.
도구영향 포인트
esbuild / swc원래 타입 체크를 하지 않고 트랜스파일만 함 → 영향 적음. 타입 체크는 별도 스텝
ts-nodeTS 컴파일러 API 의존 → 새 API 안정화 전까지 호환성 확인 필요
webpack (ts-loader)내부 API 사용 방식에 따라 영향 → 로더 버전 호환 확인
ESLint (typescript-eslint)타입 정보 기반 룰은 파서 연동 방식 확인 필요

여기서 중요한 실무 감각. 상당수 팀은 이미 트랜스파일(esbuild/swc)과 타입 체크(tsc)를 분리해서 운영한다. 이런 구조라면 타입 체크 스텝만 tsgo로 실험 삼아 바꿔보기가 훨씬 수월하다. 반대로 ts-loaderts-node로 컴파일과 실행을 통째로 묶어놓은 레거시 구성은 마이그레이션 난이도가 올라간다.

흔한 함정 1: tsconfig 옵션 미지원/차이

Go 재작성 초기 단계에서는 일부 컴파일러 옵션이나 엣지 케이스 동작이 기존과 다를 수 있다. 알 수 없는 옵션을 만나면 이런 식으로 튕긴다.

error: Unknown compiler option 'someLegacyOption'.

# 또는 tsconfig 파싱 단계에서
error TS5023: Unknown compiler option '...'.

대응: 기존 tsc --noEmittsgo --noEmit에러 개수·에러 코드를 diff로 비교해서 결과가 갈리는 파일이 있는지 먼저 확인한다. 결과가 다르면 그 파일은 옵션 해석 차이 또는 미지원 케이스일 가능성이 높다.

# 두 결과를 파일로 떨궈서 비교
npx tsc   --noEmit -p tsconfig.json 2> tsc.log   || true
npx tsgo  --noEmit -p tsconfig.json 2> tsgo.log  || true
diff <(sort tsc.log) <(sort tsgo.log)

흔한 함정 2: CI 캐시 키를 안 바꿔서 옛날 결과가 통과됨

컴파일러를 바꿨는데 CI 캐시 키에 컴파일러 버전이 안 들어가 있으면, 예전 캐시가 적중해서 "실제로는 안 돌았는데 통과한 것처럼" 보이는 사고가 난다. 이건 도구 문제가 아니라 파이프라인 설계 문제인데, 컴파일러 교체 시점에 특히 잘 터진다. 캐시 키에 컴파일러 패키지 버전을 반드시 포함시켜라.

CI/CD 마이그레이션은 병렬 검증 스텝으로 시작하라

한 번에 갈아엎지 말고, 기존 tsc는 그대로 두고 tsgo를 non-blocking 스텝으로 추가하는 게 안전하다. GitHub Actions 예시.

jobs:
  typecheck:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      - run: npm ci

      # 기존: 실패하면 파이프라인 중단 (source of truth)
      - name: Type check (tsc)
        run: npx tsc --noEmit -p tsconfig.json

      # 신규: 결과만 관찰, 실패해도 파이프라인 안 막음
      - name: Type check (tsgo, experimental)
        run: npx tsgo --noEmit -p tsconfig.json
        continue-on-error: true

이 상태로 몇 주 돌려보면서 두 스텝의 결과가 지속적으로 일치하고 시간 이득이 유의미하면, 그때 tsgo를 정식 스텝으로 승격하고 tsc를 내리면 된다. 인프라 관점에서 이런 "그림자 실행(shadow run)" 패턴은 리스크를 크게 줄여준다.

대안

지금 당장 타입 체크 속도만 급하고 리스크를 최소화하고 싶다면, 컴파일러 교체 대신 다음도 고려할 수 있다.

  • tsc --build프로젝트 레퍼런스 + incremental 설정으로 증분 빌드 최적화
  • 트랜스파일은 esbuild/swc로 넘기고 타입 체크만 별도 스텝으로 분리(이미 표준적인 구성)
  • 모노레포라면 Turborepo/Nx 캐시로 타입 체크 결과 자체를 캐싱

4. 정리: 한 줄 요약 + 누가 언제 써야 하는가

한 줄 요약: TypeScript 7은 Go로 재작성해 속도·병렬성을 노린 컴파일러이고, 지금은 programmatic API가 완성되기 전(7.1 예정)이라 "관찰용으로 붙여보되 소스 오브 트루스는 아직 기존 tsc"로 두는 게 안전하다.

  • 지금 실험해도 좋은 팀: 트랜스파일과 타입 체크가 이미 분리돼 있고, CI 타입 체크 시간이 병목인 대형 프로젝트. non-blocking 스텝으로 붙여 비교하기 좋다.
  • 서두르지 말아야 할 팀: ts-node/ts-loader나 커스텀 트랜스포머 등 컴파일러 내부 API에 강하게 의존하는 도구를 쓰는 곳. 7.1 API 안정화 이후를 보는 게 낫다.
  • 공통 원칙: 벤치마크는 남의 걸 믿지 말고 본인 레포에서 time으로 직접 재라. 에러 개수/코드가 기존과 일치하는지 diff로 검증하는 절차를 반드시 넣어라.

정리하면, 이건 "버전 올려서 끝"이 아니라 컴파일러 아키텍처가 바뀐 사건이다. 프론트 팀만의 이슈로 넘기지 말고, 빌드 시간과 CI 비용을 보는 인프라/백엔드 입장에서 미리 그림자 실행으로 데이터를 쌓아두면 나중에 전환 결정할 때 근거가 된다.

참고 자료

※ 벤치마크 수치와 세부 옵션 지원 범위는 버전에 따라 달라질 수 있으니, 도입 전 반드시 공식 문서와 본인 레포 측정 결과를 기준으로 판단하기 바란다.

728x90

+ Recent posts