TTFB Request Duration: 서버 처리 시간 단축하기
Request duration은 서버가 요청을 처리하는 데 소요되는 시간입니다. 대부분의 사이트에서 TTFB의 가장 큰 원인입니다. Server-Timing, 캐싱, 데이터베이스 최적화 및 플랫폼 수정 사항에 대해 알아보세요.

Time to First Byte의 Request Duration 하위 부분 단축하기
이 문서는 Time to First Byte (TTFB) 가이드의 일부입니다. Request duration은 TTFB의 다섯 번째이자 마지막 하위 부분입니다. 이는 서버가 실제로 요청을 처리하는 데(수신, 애플리케이션 로직 실행, 데이터베이스 쿼리, HTML 응답 생성) 소요되는 시간을 측정합니다. 모든 TTFB 하위 부분 중 request duration은 여러분이 가장 많이 제어할 수 있는 부분입니다. CoreDash로 사이트를 감사한 제 경험에 따르면, 이는 대다수의 웹사이트에서 느린 TTFB를 유발하는 가장 큰 단일 원인입니다.
Time to First Byte (TTFB)는 다음과 같은 하위 부분으로 나눌 수 있습니다:
- Waiting + Redirect (또는 waiting duration)
- Worker + Cache (또는 cache duration)
- DNS (또는 DNS duration)
- Connection (또는 connection duration)
- Request (또는 request duration)
Time to First Byte를 최적화하고 싶으신가요? 이 문서는 Time to First Byte의 request duration 부분에 초점을 맞춥니다. request duration이 무엇인지 확실하지 않다면 먼저 Time to First Byte란 무엇인가 및 TTFB 문제 식별 및 해결부터 시작하세요.
Table of Contents!
- Time to First Byte의 Request Duration 하위 부분 단축하기
- Request Duration 동안 일어나는 일
- Server-Timing API로 Request Duration을 측정하는 방법
- 흔한 Request Duration 병목 현상
- 플랫폼별 솔루션
- 데이터베이스 커넥션 풀링 (Database Connection Pooling)
- 리버스 프록시 캐싱 (Reverse Proxy Caching)
- Request Duration을 위한 에지 컴퓨팅 (Edge Computing)
- JavaScript로 Request Duration 측정하기
- RUM 데이터로 Request Duration 문제를 식별하는 방법
- 우리의 데이터가 보여주는 것
- 추가 읽을거리: 최적화 가이드
- TTFB 하위 부분: 전체 가이드
Request Duration 동안 일어나는 일
Request duration은 서버가 HTTP 요청을 수신하는 순간 시작되어 응답의 첫 번째 바이트를 다시 보낼 때 끝납니다. 그 사이에 서버가 수행하는 모든 작업이 request duration에 포함됩니다. 일반적인 동적 웹사이트의 경우 여기에는 다음이 포함됩니다:
- Request routing: 웹 서버(Nginx, Apache, IIS)가 요청을 받아 애플리케이션 계층으로 라우팅합니다.
- Application bootstrap: 프레임워크가 초기화됩니다. 워드프레스(WordPress)에서는 코어, 활성 플러그인, 테마를 로드하는 것을 의미합니다. Node.js/Express에서는 미들웨어 실행을 의미합니다.
- Business logic: 인증 확인, 권한 검증, 데이터 변환, 템플릿 선택 등 애플리케이션 코드가 실행됩니다.
- Database queries: 애플리케이션이 MySQL, PostgreSQL, MongoDB 또는 여러분이 사용하는 데이터 저장소를 쿼리합니다. 이것이 종종 가장 느린 단계입니다.
- Response generation: 애플리케이션이 템플릿, 컴포넌트 트리 또는 직렬화 로직에서 HTML (또는 JSON) 응답을 렌더링합니다.
이러한 각 단계는 시간을 추가합니다. 잘 최적화된 서버는 100ms 미만으로 전체 체인을 완료합니다. 최적화가 제대로 되지 않은 서버는 800ms 이상 걸릴 수 있으며, 이는 DNS, connection, 네트워크 지연 시간이 개입하기도 전의 이야기입니다.
Server-Timing API로 Request Duration을 측정하는 방법
Server-Timing HTTP 헤더는 request duration을 진단하는 데 가장 좋은 도구입니다. 이 헤더를 사용하면 서버에서 브라우저로 직접 타이밍 분석 데이터를 보낼 수 있으며, 브라우저의 개발자 도구(DevTools)에 표시되고 Performance API를 통해 접근할 수 있습니다. 즉, 개별 단계(데이터베이스 쿼리, 템플릿 렌더링, 캐시 조회 등)를 측정하고 정확히 어느 부분이 느린지 확인할 수 있습니다.
Server-Timing 헤더 형식은 다음과 같습니다:
Server-Timing: db;dur=53.2;desc="Database queries",
app;dur=24.1;desc="Application logic",
tpl;dur=18.7;desc="Template rendering" 이 값들은 Chrome 개발자 도구의 Network 패널의 "Timing" 탭에 나타나며, 여러분의 코드에 직접 매핑되는 서버 측 폭포수(waterfall)를 제공합니다.
PHP에서의 Server-Timing
PHP의 hrtime(true) 함수는 나노초 정밀도를 제공하여 개별 서버 측 작업을 측정하는 데 이상적입니다. PHP 애플리케이션에 Server-Timing을 추가하는 방법은 다음과 같습니다:
// 나노초 정밀도로 데이터베이스 쿼리 시간 측정
$dbStart = hrtime(true);
$result = $pdo->query('SELECT * FROM products WHERE active = 1');
$dbDuration = (hrtime(true) - $dbStart) / 1e6; // ms 단위로 변환
// 템플릿 렌더링 측정
$tplStart = hrtime(true);
$html = renderTemplate('product-list', ['products' => $result]);
$tplDuration = (hrtime(true) - $tplStart) / 1e6;
// 브라우저로 Server-Timing 헤더 전송
header(sprintf(
'Server-Timing: db;dur=%.1f;desc="Database", tpl;dur=%.1f;desc="Template"',
$dbDuration,
$tplDuration
)); Node.js / Express에서의 Server-Timing
Node.js에서는 고해상도 타이밍을 위해 process.hrtime.bigint()를 사용하세요. 미들웨어 패턴을 사용하면 총 요청 처리 시간과 개별 작업을 측정할 수 있습니다:
// Server-Timing 헤더를 위한 Express 미들웨어
app.use((req, res, next) => {
const start = process.hrtime.bigint();
const timings = [];
// 응답 객체에 헬퍼 연결
res.serverTiming = (name, desc) => {
const mark = process.hrtime.bigint();
return () => {
const dur = Number(process.hrtime.bigint() - mark) / 1e6;
timings.push(`${name};dur=${dur.toFixed(1)};desc="${desc}"`);
};
};
// 응답을 보내기 전에 헤더 추가
const originalEnd = res.end.bind(res);
res.end = (...args) => {
const total = Number(process.hrtime.bigint() - start) / 1e6;
timings.push(`total;dur=${total.toFixed(1)};desc="Total"`);
res.setHeader('Server-Timing', timings.join(', '));
originalEnd(...args);
};
next();
});
// 라우트 핸들러에서의 사용
app.get('/products', async (req, res) => {
const endDb = res.serverTiming('db', 'Database');
const products = await db.query('SELECT * FROM products');
endDb();
const endTpl = res.serverTiming('tpl', 'Template');
const html = renderTemplate('products', { products });
endTpl();
res.send(html);
}); 흔한 Request Duration 병목 현상
수백 대의 서버를 분석해 본 결과, 같은 병목 현상이 반복해서 나타나는 것을 확인했습니다. 다음은 느린 request duration의 가장 흔한 원인들을 제가 자주 접하는 순서대로 나열한 것입니다:
- 느린 데이터베이스 쿼리: 인덱싱되지 않은 쿼리, N+1 쿼리 패턴, 큰 테이블의 전체 테이블 스캔(full table scan). 이것이 가장 큰 원인입니다. WooCommerce 상품 테이블에 인덱스가 하나만 없어도 요청당 300~500ms가 추가될 수 있습니다.
- 페이지 캐싱 누락: 콘텐츠가 변경되지 않았음에도 모든 방문자를 위해 동일한 HTML을 다시 생성하는 경우입니다. 이는 객체 캐시나 페이지 캐시 플러그인이 없는 워드프레스 사이트에서 특히 흔합니다.
- 비용이 많이 드는 연산: 복잡한 계산, 이미지 처리 또는 데이터 집계 결과를 캐시하지 않고 매 요청 시마다 실행하는 경우입니다.
- 외부 API 호출: 써드파티 API(결제 게이트웨이, CRM 시스템, 재고 서비스)에 대한 동기식 요청으로, 완료될 때까지 응답을 차단합니다.
- 최적화되지 않은 애플리케이션 코드: 사용하지 않는 모듈을 로드하거나, 불필요한 미들웨어를 실행하거나, 데이터 처리에 비효율적인 알고리즘을 사용하는 경우입니다.
- 콜드 스타트(Cold starts): 서버리스 플랫폼(AWS Lambda, Cloudflare Workers, Vercel Functions)에서 비활성 기간 이후의 첫 번째 요청은 컨테이너 초기화 오버헤드를 발생시킵니다.
플랫폼별 솔루션
워드프레스 (WordPress)
워드프레스 사이트는 모든 페이지 로드 시 전체 워드프레스 코어, 모든 활성 플러그인, 테마를 부트스트랩하기 때문에 느린 request duration에 특히 취약합니다. 다음은 가장 큰 차이를 만드는 요소들입니다:
- 영구적인 객체 캐시 설치: Redis 또는 Memcached를 사용하세요. 워드프레스는 페이지 로드당 수십 개의 데이터베이스 쿼리를 실행하며, 그 중 다수는 방문자 간에 동일합니다. (Redis Object Cache와 같은 플러그인을 통한) 객체 캐시는 이러한 결과를 메모리에 저장하여 데이터베이스 처리 시간을 60~80% 단축합니다.
- 전체 페이지 캐싱 활성화: 서버 수준의 페이지 캐시(Nginx FastCGI Cache, Varnish) 또는 WP Super Cache와 같은 플러그인을 사용하세요. 이는 PHP를 전혀 건드리지 않고 캐시된 HTML을 직접 제공합니다.
- 느린 플러그인 감사: Query Monitor 플러그인을 사용하여 어떤 플러그인이 가장 많은 데이터베이스 쿼리를 생성하거나 PHP 실행 시간을 가장 많이 소비하는지 식별하세요.
- WooCommerce 쿼리 최적화: WooCommerce는 느린 상품 쿼리로 악명이 높습니다. HPOS(High-Performance Order Storage) 기능을 활성화하고
wp_postmeta테이블에 적절한 데이터베이스 인덱스를 추가하세요.
사례 연구: 한 워드프레스 사이트는 객체 캐싱(Redis)을 구현하고 매 페이지 로드 시 실행되던 느린 WooCommerce 상품 쿼리를 최적화하여 request duration을 800ms에서 120ms로 단축했습니다. 인덱스 없이 80,000개의 postmeta 행에 대해 전체 테이블 스캔을 수행하고 있었기 때문에 상품 쿼리 단독으로 450ms를 차지하고 있었습니다.
Node.js
Node.js 애플리케이션은 기본적으로 빠른 request duration을 가지지만, 규모가 커지거나 차단(blocking) 작업 시 문제가 발생합니다:
- 내장 검사기로 프로파일링:
node --inspect app.js를 실행하고 Chrome DevTools를 연결하여 CPU 집약적인 함수를 식별하세요. 이벤트 루프를 차단하는 동기식 작업을 찾아보세요. - ORM 쿼리 로깅 활성화: Sequelize, Prisma 또는 TypeORM을 사용하는 경우 쿼리 로깅을 활성화하여 N+1 패턴과 느린 쿼리를 찾으세요. Sequelize의 경우:
sequelize = new Sequelize({ logging: console.log }). - 커넥션 풀링(Connection pooling) 사용: 매 요청마다 새로운 데이터베이스 연결을 생성하지 마세요. (대부분의 Node.js 데이터베이스 드라이버에 내장된) 커넥션 풀을 사용하여 연결을 재사용하세요.
- 인메모리 캐싱 구현: 자주 변경되지 않고 자주 액세스하는 데이터의 경우, 매 요청마다 데이터베이스에 접근하는 것을 피하기 위해 LRU 캐시를 사용하세요.
PHP (워드프레스 제외)
Laravel, Symfony 또는 사용자 정의 PHP 애플리케이션의 경우:
- OPcache 활성화: PHP의 OPcache는 PHP 스크립트를 바이트코드로 컴파일하고 이를 공유 메모리에 저장하여, 매 요청마다 파싱하고 컴파일할 필요를 없애줍니다. 이것만으로도 request duration을 30~50%까지 줄일 수 있습니다.
- 바이트코드 프리로더(Preloader) 사용: PHP 8.0 이상에서는 프리로딩을 지원하며(PHP 7.4부터 사용 가능), 이는 서버 시작 시 프레임워크 파일을 메모리에 로드하여 요청당 로드할 필요가 없게 합니다.
- Xdebug 또는 Blackfire로 프로파일링: 가장 많은 경과 시간(wall-clock time)을 소비하는 특정 함수를 식별하세요.
Python (Django / Flask)
Python 애플리케이션은 종종 Node.js나 PHP에 비해 더 높은 기본 request duration을 갖습니다:
- 비동기 프레임워크 사용: I/O 대기가 병목이라면, 데이터베이스 쿼리와 API 호출을 동시에 처리하기 위해 FastAPI 또는 ASGI가 포함된 Django를 고려해 보세요.
- Django의 데이터베이스 쿼리 로깅 활성화: 설정(settings)에서
LOGGING을 설정하여 모든 SQL 쿼리를 기록하고 느리거나 중복된 쿼리를 식별하세요. select_related()및prefetch_related()사용: 관련 테이블을 조인(join)하도록 명시적으로 지시하지 않으면 Django의 ORM은 N+1 쿼리를 기꺼이 생성할 것입니다.
데이터베이스 커넥션 풀링 (Database Connection Pooling)
매 요청마다 새로운 데이터베이스 연결을 여는 것은 느린 request duration의 가장 흔하면서도 가장 피하기 쉬운 원인 중 하나입니다. 각각의 새로운 연결은 TCP 핸드셰이크, 인증 및 세션 초기화를 필요로 하며, 이는 요청당 5~30ms를 추가할 수 있습니다. 부하가 걸린 상황에서 데이터베이스 서버의 사용 가능한 연결이 고갈되면 이는 재앙적인 결과를 초래합니다.
커넥션 풀링은 요청 간에 재사용되는 열린 연결의 풀을 유지하여 이 문제를 해결합니다. 애플리케이션은 풀에서 연결을 빌려 쿼리를 실행하고, 완료되면 연결을 반환합니다. 이를 통해 요청당 연결 오버헤드를 완전히 제거할 수 있습니다.
대부분의 데이터베이스 드라이버는 커넥션 풀링을 기본적으로 지원합니다. 예를 들어, pg (PostgreSQL)를 사용하는 Node.js에서는 다음과 같이 설정할 수 있습니다:
const { Pool } = require('pg');
const pool = new Pool({
host: 'localhost',
database: 'myapp',
max: 20, // 풀의 최대 연결 수
idleTimeoutMillis: 30000, // 30초 후 유휴 연결 닫기
connectionTimeoutMillis: 2000 // 사용 가능한 연결이 없으면 빠르게 실패
});
// 새로운 클라이언트를 생성하는 대신 pool.query() 사용
const result = await pool.query('SELECT * FROM products WHERE id = $1', [id]); PHP-FPM 환경의 PHP 애플리케이션의 경우 영구 연결(PDO::ATTR_PERSISTENT)을 사용하거나 PostgreSQL용 PgBouncer, MySQL용 ProxySQL과 같은 외부 풀러(pooler) 사용을 고려해 보세요.
리버스 프록시 캐싱 (Reverse Proxy Caching)
가장 빠른 응답은 애플리케이션이 아예 생성할 필요가 없는 응답입니다. 리버스 프록시 캐시(Varnish, Nginx FastCGI Cache 또는 CDN 에지 캐시)는 애플리케이션 서버 앞에 위치하여 캐시된 응답을 메모리에서 직접 제공합니다. 방문자 간에 변경되지 않는 페이지(대부분의 웹사이트에 있는 대다수의 페이지가 이에 해당)의 경우, 이는 request duration을 거의 0으로 낮춥니다.
- Varnish: 메모리에서 초당 수천 개의 요청을 처리할 수 있는 전용 HTTP 캐시입니다. 애플리케이션 응답에
Cache-Control헤더를 설정하면 Varnish가 나머지를 처리합니다. - Nginx FastCGI Cache: 이미 Nginx를 웹 서버로 사용하고 있다면, PHP-FPM 응답을 위해 내장 캐싱을 활성화하세요. 이렇게 하면 스택에 다른 계층을 추가하지 않아도 됩니다.
- CDN 에지 캐싱: Cloudflare, Fastly, Amazon CloudFront와 같은 서비스는 전 세계의 에지 위치에 HTML을 캐시할 수 있어 request duration과 네트워크 지연 시간을 동시에 줄여줍니다.
효과적인 리버스 프록시 캐싱의 핵심은 적절한 Cache-Control 헤더입니다. 공유 캐시 TTL(Time-to-Live)에 대해 s-maxage를 설정하고 백그라운드에서 캐시가 새로 고쳐지는 동안 오래된 콘텐츠를 제공하기 위해 stale-while-revalidate를 사용하세요:
Cache-Control: public, s-maxage=3600, stale-while-revalidate=60 Request Duration을 위한 에지 컴퓨팅 (Edge Computing)
Cloudflare Workers 및 Vercel Edge Functions와 같은 에지 컴퓨팅 플랫폼은 사용자에게 물리적으로 가까운 CDN 에지 위치에서 서버 측 코드를 실행합니다. 에지 컴퓨팅이 코드를 더 빠르게 만들지는 않지만, 사용자와 오리진 서버 간의 네트워크 지연 시간을 제거합니다. 이는 오리진 데이터 센터에서 멀리 떨어진 사용자에게 가장 중요합니다.
에지 함수는 다음의 경우에 가장 적합합니다:
- 무거운 데이터베이스 액세스가 필요 없는 가벼운 API 응답
- 오리진에 도달하기 전 에지에서 적용되는 개인화 로직(A/B 테스트, 지리적 기반 콘텐츠)
- 전체 오리진 왕복 없는 HTML 재작성 및 헤더 조작
데이터베이스 쿼리가 필요한 페이지의 경우, 에지 컴퓨팅만으로는 request duration 문제를 해결할 수 없습니다. 진정한 이점은 전체 요청 수명 주기를 사용자 가까이에 유지하기 위해 에지 함수를 전 세계적으로 분산된 데이터베이스(PlanetScale, Neon, Turso) 또는 에지 측 캐싱과 결합하는 것입니다.
JavaScript로 Request Duration 측정하기
Navigation Timing API를 사용하여 브라우저에서 직접 TTFB의 request duration 하위 부분을 측정할 수 있습니다:
new PerformanceObserver((entryList) => {
const [nav] = entryList.getEntriesByType('navigation');
const requestDuration = nav.responseStart - nav.requestStart;
console.log('Request Duration:', requestDuration.toFixed(0), 'ms');
console.log(' Request start:', nav.requestStart.toFixed(0), 'ms');
console.log(' Response start:', nav.responseStart.toFixed(0), 'ms');
if (requestDuration > 200) {
console.warn('느린 request duration이 감지되었습니다. 서버 처리 시간을 확인하세요.');
}
}).observe({
type: 'navigation',
buffered: true
}); request duration은 responseStart - requestStart로 계산됩니다. 지속적으로 200ms를 초과하는 값은 서버가 요청을 처리하는 데 너무 많은 시간을 소비하고 있음을 나타냅니다. (위에서 설명한) Server-Timing 헤더를 사용하여 서버 처리 중 어느 부분이 원인인지 식별하세요.
RUM 데이터로 Request Duration 문제를 식별하는 방법
request duration이 실제 사용자에게 미치는 영향을 이해하려면 CoreDash와 같은 Real User Monitoring (RUM) 도구가 필요합니다. RUM 데이터는 실험실 결과가 아니라 페이지, 기기, 위치에 걸쳐 방문자가 경험하는 실제 TTFB 분석 데이터를 보여줍니다.
CoreDash에서 "Time to First Byte breakdown"을 클릭하여 TTFB의 request duration 부분을 시각화하세요. request duration이 지속적으로 200ms를 초과하는 페이지를 찾으세요. 그곳이 바로 최적화 대상입니다.
우리의 데이터가 보여주는 것
CoreDash에서 모니터링하는 수천 개의 사이트 전반에 걸쳐, request duration(서버 처리 시간)은 우리 데이터 세트의 대다수 사이트에서 가장 큰 TTFB 하위 부분입니다. 이로 인해 서버 최적화는 대부분의 웹사이트에서 단일 요소로 가장 큰 TTFB 개선을 이끌어 냅니다.
전체 페이지 캐싱을 활성화한 사이트의 중간(median) request duration은 약 35ms입니다. 캐싱이 전혀 없는 사이트의 중간값은 약 320ms입니다. 이 격차(거의 10배)는 무엇보다도 먼저 서버 측 캐싱에 투자해야 한다는 제가 제시할 수 있는 가장 강력한 주장입니다.
Request duration은 Core Web Vitals의 진단 지표인 Time to First Byte의 일부입니다. TTFB 문제를 식별하고 해결하는 전체 가이드를 보려면 TTFB 식별 및 해결 가이드를 참조하세요. 또한 포괄적인 최적화 개요를 보려면 궁극의 Core Web Vitals 체크리스트를 확인할 수도 있습니다.
추가 읽을거리: 최적화 가이드
request duration 최적화를 보완하는 관련 최적화 기법에 대해서는 다음 가이드를 살펴보세요:
- 103 Early Hints: 서버가 여전히 요청을 처리하는 동안 브라우저에 리소스 힌트(preload, preconnect)를 보내어 체감되는 TTFB를 줄입니다.
- 성능을 위한 Cloudflare 구성: 전 세계적으로 request duration을 줄이기 위해 CDN 에지 캐싱과 최적화된 서버 구성을 사용하세요.
TTFB 하위 부분: 전체 가이드
Request duration은 TTFB의 다섯 가지 하위 부분 중 하나입니다. 전체적인 그림을 이해하려면 다른 하위 부분들도 살펴보세요:
- TTFB 문제 식별 및 해결: 모든 TTFB 최적화를 위한 진단의 출발점입니다.
- Waiting Duration: 리디렉션, 브라우저 대기열(queuing) 및 HSTS 최적화.
- Cache Duration: 서비스 워커 성능, 브라우저 캐시 조회 및 bfcache.
- DNS Duration: DNS 제공업체 선택, TTL 구성 및 dns-prefetch.
- Connection Duration: TCP 핸드셰이크, TLS 최적화, HTTP/3 및 preconnect.

