performance2025-05-12·7·70/348

Express.js compression 미들웨어 설정 및 최적화

Express.js에서 compression 미들웨어를 사용하여 응답 크기를 줄이고 성능을 향상시키는 방법을 다룹니다.

Introduction

웹 애플리케이션에서 응답 크기를 줄이는 것은 성능 최적화의 핵심 요소입니다. Express.js에서 compression 미들웨어를 사용하면 HTTP 응답을 자동으로 압축하여 전송 속도를 향상시킬 수 있습니다.

Environment

# 프로젝트 의존성
npm list express compression
# express@4.18.2
# compression@1.7.4

# 브라우저 개발자 도구에서 응답 헤더 확인
# Content-Encoding: gzip

Problem

API 서버의 응답 시간이 느려지는 문제가 발생했습니다:

// 압축 없는 API 응답
app.get('/api/products', async (req, res) => {
    const products = await Product.find().limit(1000);
    // 응답 크기: 2.5MB
    // 전송 시간: 약 800ms
    res.json(products);
});

측정 결과

# 응답 크기 확인
$ curl -I http://localhost:3000/api/products
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 2621440  # 2.5MB

Analysis

압축 전후 비교

항목압축 전gzipbrotli
크기2.5MB450KB380KB
전송 시간800ms180ms150ms
CPU 사용률낮음중간높음

compression 미들웨어 동작 방식

클라이언트 요청 → compression 미들웨어 → 핸들러 → 응답 압축 → 클라이언트

Solution

1. 기본 compression 설정

const express = require('express');
const compression = require('compression');

const app = express();

// 모든 응답에 gzip 압축 적용
app.use(compression());

// API 라우트
app.get('/api/products', async (req, res) => {
    const products = await Product.find().limit(1000);
    res.json(products);
});

2. 필터 함수로 특정 경로 제외

app.use(compression({
    // 1KB 미만은 압축하지 않음
    threshold: 1024,

    // 필터 함수: 어떤 응답을 압축할지 결정
    filter: (req, res) => {
        // 이미 압축된 응답은 제외
        if (req.headers['x-no-compression']) {
            return false;
        }

        // 이미 Content-Encoding이 설정된 경우 제외
        if (res.getHeader('Content-Encoding')) {
            return false;
        }

        // 특정 경로 제외
        if (req.path.startsWith('/static/images/')) {
            return false;
        }

        // 기본 필터 적용
        return compression.filter(req, res);
    },
}));

3. Brotli 압축 설정

const zlib = require('zlib');

app.use(compression({
    // Brotli 사용 (지원되는 경우)
    brotli: {
        enabled: true,
        zlib: {
            params: {
                [zlib.constants.BROTLI_PARAM_QUALITY]: 4, // 0-11, 높을수록 압축률↑ 속도↓
                [zlib.constants.BROTLI_PARAM_SIZE_HINT]: 1024 * 1024, // 1MB
                [zlib.constants.BROTLI_PARAM_MODE]: zlib.constants.BROTLI_MODE_GENERIC,
            },
        },
    },
    threshold: 1024,
}));

4. Content-Type 필터링

app.use(compression({
    filter: (req, res) => {
        const contentType = res.getHeader('content-type') || '';
        const type = contentType.toLowerCase();

        // 압축 대상 Content-Type 목록
        const compressibleTypes = [
            'text/',
            'application/json',
            'application/javascript',
            'application/xml',
            'application/rss+xml',
            'application/atom+xml',
            'image/svg+xml',
        ];

        // 압축 가능한 타입인지 확인
        const shouldCompress = compressibleTypes.some(t => type.includes(t));

        // 바이너리 파일은 제외 (이미 압축된 경우)
        if (type.includes('image/') && !type.includes('image/svg')) {
            return false;
        }

        if (type.includes('video/') || type.includes('audio/')) {
            return false;
        }

        return shouldCompress;
    },
    threshold: 1024,
}));

5. 개발 환경과 프로덕션 환경 분리

const express = require('express');
const compression = require('compression');

const app = express();

// 개발 환경에서는 압축 비활성화
if (process.env.NODE_ENV === 'production') {
    app.use(compression({
        level: 6, // gzip 압축 레벨 (1-9)
        threshold: 1024,
        filter: (req, res) => {
            if (req.headers['x-no-compression']) {
                return false;
            }
            return compression.filter(req, res);
        },
    }));
}

// 정적 파일 압축 설정
app.use('/static', express.static('public', {
    maxAge: '1d',
    etag: true,
    lastModified: true,
}));

6. 성능 측정 미들웨어

app.use((req, res, next) => {
    const start = Date.now();
    const originalSend = res.send;

    res.send = function(body) {
        const duration = Date.now() - start;
        const originalSize = Buffer.byteLength(body);
        const compressed = res.getHeader('Content-Encoding');
        const finalSize = compressed ?
            parseInt(res.getHeader('Content-Length') || '0') : originalSize;

        console.log({
            path: req.path,
            originalSize,
            compressedSize: finalSize,
            compressionRatio: compressed ?
                ((1 - finalSize / originalSize) * 100).toFixed(2) + '%' : 'N/A',
            duration: duration + 'ms',
        });

        originalSend.call(this, body);
    };

    next();
});

Lessons Learned

  1. 적절한 threshold 설정: 너무 작은 응답은 압축하지 않아야 CPU 낭비를 방지할 수 있습니다
  2. Brotli 활용: 최신 브라우저에서는 Brotli가 gzip보다 더 나은 압축률을 제공합니다
  3. Content-Type 필터링: 바이너리 파일이나 이미 압축된 파일은 제외하세요
  4. 개발 환경 고려: 개발 시에는 압축을 비활성화하여 디버깅 편의성을 높이세요
  5. 모니터링: 압축률과 응답 시간을 지속적으로 모니터링하세요

This blog does not accept any external sponsorships, affiliate marketing, or ad revenue.