서버 및 호스팅 성능 참고 글

서버 응답 시간 줄이기: TTFB를 진단하고 개선하는 방법

호스팅, 캐시, 전달 설정을 바꾸기 전에 연결 지연과 애플리케이션 작업을 구분합니다.

브라우저와 서버를 연결한 추상적인 요청 타임라인에서 대기 구간이 강조되어 있습니다.

서버 응답 시간을 줄이려면 요청이 어디에서 기다리는지 측정한 뒤 해당 구간을 고쳐야 합니다. 첫 바이트가 늦는 원인은 리디렉션, 연결 설정, 먼 원본 서버, 애플리케이션 작업, 데이터베이스 쿼리, 캐시 미스 등 다양합니다. 원인을 구분하지 않고 호스팅부터 바꾸면 같은 병목을 더 비싼 서버로 옮기는 데 그칠 수 있습니다.

Time to First Byte, 즉 TTFB는 탐색 요청에서 브라우저가 응답의 첫 바이트를 받을 때까지 걸린 시간입니다. 애플리케이션이 HTML을 만드는 시간보다 범위가 넓습니다. 실제 목표는 유용한 콘텐츠를 더 빨리 전달하는 것이므로 첫 바이트뿐 아니라 렌더링에 미치는 영향도 확인해야 합니다.

목표를 정하기 전에 측정의 범위를 정의합니다

Google의 TTFB 최적화 지침은 800밀리초 이하를 대략적인 좋음 목표로 제시합니다. TTFB 자체는 세 가지 Core Web Vitals에 포함되지 않습니다. 이보다 크면 조사할 필요가 있지만, 최종 판단에는 의미 있는 콘텐츠가 얼마나 빨리 보이는지도 포함해야 합니다.

서버 렌더링 페이지는 HTML 준비에 더 오래 걸려도 빈 구조만 즉시 반환하는 클라이언트 렌더링 페이지보다 유용한 내용을 먼저 보여 줄 수 있습니다. 서버 작업이 느려도 좋다는 뜻은 아닙니다. 유용한 작업을 모두 JavaScript 실행 이후로 미뤄 첫 바이트만 줄이는 것은 좋지 않은 절충일 수 있다는 뜻입니다.

측정 범위도 서로 다릅니다. 현장의 탐색 TTFB에는 최종 문서에 대한 실험실 감사가 제외하는 지연이 포함될 수 있습니다. 기존 Lighthouse의 서버 응답 시간 감사는 DNS, 리디렉션, 연결 전체를 나타내지 않았습니다. 현재 Chrome 문서는 Document request latency 인사이트도 사용하므로 오래된 안내의 감사 이름이 보고서와 다를 수 있습니다.

최적화하려는 값이 현장 탐색 TTFB인지, 브라우저 요청의 대기 구간인지, 애플리케이션 타이머인지, 명령줄 첫 바이트 결과인지 적습니다. 서로 바꿔 쓸 수 있는 값처럼 비교하면 각각의 측정이 맞더라도 결론은 충돌할 수 있습니다.

탐색부터 유용한 HTML까지 요청을 추적합니다

첫 유용한 문서 응답 이전에는 여러 단계가 있을 수 있습니다. 비용은 DNS 캐시, 기존 연결, 리디렉션, 프로토콜, 전달 인프라의 경로에 따라 달라집니다.

단계 지연 원인 확인할 근거
리디렉션 호스트, 스킴, 경로의 반복 변경 상태 코드와 리디렉션 체인
DNS와 연결 설정 이름 확인 지연, 거리, 새 연결 브라우저 요청 타이밍
TLS 협상 새 보안 연결과 네트워크 왕복 연결 시간과 재사용
엣지 처리 캐시 조회, 규칙, 원본 요청 CDN 응답 헤더와 로그
원본 대기열과 실행 바쁜 worker, 데이터베이스, 외부 서비스 애플리케이션 트레이스와 서버 지표
응답 전달 버퍼링, 거리, 전송 방식 첫 바이트 시간과 워터폴

모든 요청에 모든 단계가 생긴다고 가정하지 않습니다. 재방문자는 기존 연결을 재사용할 수 있습니다. 캐시 적중은 애플리케이션 실행을 생략할 수 있습니다. 개인 계정 페이지의 요청에 다른 사람을 위한 HTML이 재사용되어서는 안 됩니다.

같은 콘텐츠로 가는 두 가지 정상적인 경로를 비교하는 것이 가장 유용할 때가 많습니다. 캐시가 준비된 공개 페이지와 캐시되지 않은 페이지는 시간이 크게 다를 수 있습니다. 그 차이는 어느 계층이 느린 원본 작업으로부터 방문자를 보호하는지 보여 줍니다.

실제 방문 경로로 브라우저 기준값을 만듭니다

탐색 전에 DevTools를 열고 Network 패널에서 문서 요청을 확인합니다. 최종 URL, 상태, 프로토콜, 응답 헤더, 시간 구간을 기록합니다. 리디렉션도 표시합니다. 최종 목적지에서 바로 시작한 테스트로는 캠페인이나 오래된 내부 링크의 앞선 리디렉션 지연을 설명할 수 없습니다.

콜드 테스트와 웜 테스트를 구분합니다. 브라우저 캐시 비활성화는 브라우저의 재사용을 바꾸지만 CDN이나 서버 페이지 캐시를 반드시 우회하지는 않습니다. 시크릿 창도 엣지 캐시가 비어 있음을 보장하지 않습니다. 어떤 캐시 계층을 통제하려는지 명확히 합니다.

대표 콘텐츠를 검사합니다. 인기 있는 홈페이지는 거의 항상 캐시되어 있지만 오래된 기사는 요청 시 생성될 수 있습니다. 검색 결과, 계정, 결제는 공개 콘텐츠와 다른 경로를 사용할 수 있습니다. 인증된 테스트는 비공개로 유지하고 공유 보고서에 세션 쿠키를 넣지 않습니다.

적절한 테스트 서비스나 모니터링 환경이 있다면 주요 방문 지역에서도 반복합니다. 원본과 가까운 컴퓨터의 빠른 요청 하나는 먼 방문자의 경험에 대한 약한 근거입니다. 단일 측정을 전 세계 결과처럼 표현하지 말고 위치와 조건을 설명합니다.

curl로 누적 시간 지점을 구분합니다

다음 명령은 GET 요청을 보내고 응답 본문은 버리며 누적 시간 지점을 출력합니다. 예시 URL을 테스트 권한이 있는 공개 페이지로 바꿉니다. 의도적으로 리디렉션을 따라가지 않으므로 측정 요청이 조용히 바뀌지 않고 리디렉션이 드러납니다.

bash
curl --silent --show-error --output /dev/null \
  --write-out 'status=%{http_code}\ndns=%{time_namelookup}\nconnect=%{time_connect}\ntls=%{time_appconnect}\nfirst_byte=%{time_starttransfer}\ntotal=%{time_total}\n' \
  'https://example.com/'

각 값은 전송 시작 이후 경과 시간입니다. 서로 더하면 안 됩니다. 단순한 새 HTTPS 연결에서는 인접 지점의 차이가 연결과 TLS 설정을 구분하는 데 도움이 됩니다. 다만 프록시, 연결 재사용, 프로토콜 동작에 따라 해석이 달라질 수 있습니다.

연결 설정 이후부터 첫 바이트까지도 순수한 애플리케이션 시간은 아닙니다. 네트워크와 서버 작업이 함께 들어갑니다. 지연이 있을 법한 구간을 좁힌 뒤 서버 계측으로 데이터베이스 실행, 렌더링, 대기열을 구분합니다.

성능을 해석하기 전에 반환 상태를 확인합니다. 빠른 리디렉션, 오류 페이지, 봇 검증 화면은 정상 페이지가 빠르다는 뜻이 아닙니다. 애플리케이션이 다르게 처리할 수 있다면 편의를 위해 GET을 HEAD로 바꾸지도 않습니다.

비교 가능한 여러 결과를 날짜와 배포 버전과 함께 저장합니다. 중앙값은 작은 집합 비교에 도움이 되지만 개별 값과 특이한 실패도 남깁니다. 대표 숫자 하나는 사용자가 실제로 느끼는 간헐적인 지연을 숨길 수 있습니다.

비공개 정보를 노출하지 않고 애플리케이션을 계측합니다

서버 트레이스는 브라우저 타이밍이 구분하지 못하는 작업을 찾습니다. 데이터베이스 쿼리, 템플릿 렌더링, 외부 요청, 캐시 읽기, worker 대기, 파일 접근을 살펴봅니다. 버전이 달라져도 같은 단계 이름을 유지하면 계측 구조를 다시 만들지 않고 변경을 비교할 수 있습니다.

Server-Timing 응답 헤더는 이름이 붙은 소요 시간을 브라우저 도구에 제공합니다. 다음 완전한 PHP 예시는 작은 공개 HTML 응답의 생성 시간을 측정합니다. PHP 7.3부터 제공되는 hrtime을 사용하며, 운영 환경에서는 현재 지원되는 호환 런타임을 사용해야 합니다.

php
<?php
declare(strict_types=1);

$started = hrtime(true);
$title = htmlspecialchars('Performance example', ENT_QUOTES, 'UTF-8');
$html = '<!doctype html><html lang="en"><meta charset="utf-8">'
      . '<title>' . $title . '</title><h1>' . $title . '</h1></html>';
$durationMs = (hrtime(true) - $started) / 1e6;

header('Content-Type: text/html; charset=utf-8');
header('Cache-Control: no-store');
header('Server-Timing: render;dur=' . number_format($durationMs, 3, '.', ''));
echo $html;

시간은 예시를 실행할 때 측정되며 특정 벤치마크 결과를 뜻하지 않습니다. no-store는 이 진단 응답이 HTTP 캐시에 저장되지 않게 합니다. 모든 공개 페이지에 권하는 정책은 아닙니다.

전체 요청을 데이터베이스 시간이라고 이름 붙이지 말고 실제 연산을 타이머로 감쌉니다. 병렬 작업은 겹칠 수 있으므로 각 시간을 더해도 전체 응답 시간과 같지 않을 수 있습니다. 사용 가능한 worker를 기다리는 시간은 애플리케이션 타이머 시작 전에 발생할 수도 있습니다.

render, database처럼 넓은 이름을 사용합니다. 공개 헤더에 SQL, 액세스 토큰, 고객 식별자, 내부 호스트 이름, 민감한 업무 데이터를 노출하지 않습니다. 자세한 트레이스는 적절히 접근을 통제한 모니터링 시스템에 보관합니다.

비용이 큰 작업을 없애는 캐시를 선택합니다

페이지 캐시, 객체 캐시, opcode 캐시는 서로 다른 문제를 해결합니다. 함께 사용할 수 있지만 하나를 켰다고 나머지가 존재하거나 효과적이라는 뜻은 아닙니다.

캐시 재사용 대상 줄일 수 있는 작업
전체 페이지 저장 가능한 HTML 응답 공개 페이지 재생성
객체 또는 데이터 쿼리 결과나 애플리케이션 객체 반복 조회와 계산
PHP opcode 컴파일된 PHP 명령 같은 스크립트 재컴파일
브라우저 방문자 기기의 저장 가능한 응답 일부 반복 요청과 전송
CDN 엣지 방문자 가까이 저장된 응답 캐시 적중 시 원본 왕복

익명 방문자에게 같은 기사를 제공하는 매체라면 전체 페이지 캐시가 유용한 출발점입니다. 편집자가 내용을 고칠 때 어떻게 무효화되는지 확인합니다. 정정 후에도 오래된 기사가 빠르게 제공된다면 성능 보고서가 좋아도 발행 과정의 실패입니다.

동적 페이지에서는 객체 캐시가 반복 데이터 작업을 줄이면서도 애플리케이션이 개인화 응답을 만들게 할 수 있습니다. WordPress의 기본 객체 캐시는 요청 간에 자동으로 영구 유지되지 않습니다. 지속적인 백엔드에는 적절한 구현이 필요하며 효과는 실제로 무엇을 저장하는지에 달려 있습니다.

PHP OPcache는 반복 컴파일을 줄이지 느린 쿼리나 먼 네트워크 호출을 해결하지는 않습니다. 설정과 런타임 호환성을 확인하되 비효율적인 설계를 고쳐 줄 것으로 기대하지 않습니다. 관리형 플랫폼에서는 겹치는 계층을 추가하기 전에 공급자가 관리하는 캐시부터 확인합니다.

브라우저 캐시는 주로 리소스를 받은 이후 재사용에 도움이 됩니다. 새 독자의 첫 방문이 빠르다고 보장하지는 않습니다. 브라우저 캐시 가이드에서 신선도, 재검증, 저장 금지의 차이를 확인할 수 있습니다.

세션, 장바구니, 개인화 콘텐츠를 보호합니다

공유 HTML 캐시는 좁은 공개 페이지 허용 목록부터 시작합니다. 별도로 설계하고 검증한 전략이 없다면 계정, 장바구니, 결제 등 개인화 경로는 제외합니다. 세션 쿠키와 인증은 캐시 규칙에서 명시적으로 처리해야 합니다.

쿠키가 있다는 사실만으로 모든 중간 계층이 응답을 비공개로 다룬다고 보장할 수 없습니다. CDN과 애플리케이션 캐시의 동작을 읽고 실제로 검증합니다. 응답 헤더를 무시하고 강제로 저장하거나 의미를 모르는 쿼리 매개변수를 무시하는 규칙은 특히 주의합니다.

분리된 세션 두 개와 로그아웃된 브라우저로 테스트합니다. 어느 쪽에도 다른 방문자의 이름, 장바구니, 가격, 계정 상태가 나타나서는 안 됩니다. 잘못된 사람의 응답이라면 캐시 적중률은 의미가 없습니다.

캐시 키는 이해하기 쉽게 유지합니다. 캠페인 추적만 하는 매개변수는 공개 콘텐츠 캐시를 불필요하게 나눌 수 있습니다. 반면 상품 옵션, 통화, 언어, 접근 상태는 응답을 바꿀 수 있습니다. 콘텐츠나 권한에 영향을 주지 않는다고 확인한 입력만 정규화합니다.

캐시가 작동해도 느린 원본 경로를 고칩니다

캐시 미스는 계속 발생합니다. 새 콘텐츠, 드문 URL, 무효화, 다른 엣지 위치에서 들어온 트래픽은 새로운 작업을 요구할 수 있습니다. 일반 테스트에서 캐시가 원본 지연을 가려도 원본을 이해할 필요는 사라지지 않습니다.

쿼리 프로파일링으로 비싸거나 반복되는 데이터베이스 작업을 찾습니다. 인덱스를 추가하기 전에 실행 계획을 봅니다. 특정 읽기에 유리한 인덱스는 저장 공간을 쓰며 쓰기에도 영향을 줍니다. 인덱스가 많을수록 좋다고 가정하지 말고 측정한 쿼리에 근거해 바꿉니다.

반복문 안의 반복 호출, 불필요한 데이터 조회, 매번 수행되는 무거운 작업을 찾습니다. 정확성을 유지할 수 있다면 백그라운드 작업을 핵심 응답 경로에서 분리합니다. 같은 핸들러에 있다는 이유로 무관한 관리 작업이 끝날 때까지 페이지가 기다릴 필요는 없습니다.

외부 API에는 타임아웃, 적절한 캐시, 실패 시 동작이 필요합니다. 첫 응답에 꼭 필요한 데이터와 나중에 와도 되는 데이터를 구분합니다. 중요한 콘텐츠를 미루면 렌더링과 사용성에 영향이 있으므로 TTFB만 따로 최적화하지 말고 전체 사용자 작업을 평가합니다.

업그레이드 전 스테이징에서 런타임과 의존성 버전을 비교합니다. 지원되는 PHP나 Node 버전은 유지보수의 일부지만 애플리케이션의 호환성과 성능을 확인해야 합니다. 버전만 바꾸면 일정 비율 빨라진다는 보편적 약속을 제시하지 않습니다.

거리와 재사용 효과가 있을 때 CDN을 사용합니다

CDN은 방문자 가까운 엣지에서 저장 가능한 콘텐츠를 제공할 수 있습니다. 특히 캐시 적중 시 먼 원본으로 가는 왕복을 줄입니다. 하지만 정적 파일만 저장하도록 구성되어 있다면 모든 HTML 요청은 여전히 같은 느린 원본에 의존할 수 있습니다.

실제 응답과 공급자가 문서화한 캐시 상태 헤더를 봅니다. 이름과 의미는 다릅니다. 적중, 미스, 우회, 재검증, 정책에 따른 오래된 응답 중 무엇인지 확인합니다. CDN 호스트 이름이 보인다는 사실만으로 적중이라고 판단하지 않습니다.

HTML과 정적 리소스의 캐시 동작을 나누어 측정합니다. 이미지 적중률이 높아도 HTML 재사용은 나쁠 수 있습니다. 가능하면 바이트와 요청 유형도 분석합니다. 합계 비율은 개선하려는 트래픽 종류를 숨길 수 있습니다.

수명을 늘리기 전에 무효화를 계획합니다. 발행, 정정, 접근 규칙 변경, 새 빌드 배포 모두 예측 가능한 동작이 필요합니다. CDN과 엣지 캐시 가이드는 모든 공급자의 기본값이 같다고 가정하지 않고 이러한 선택을 설명합니다.

프로토콜 변경을 전달 과정의 한 부분으로 봅니다

HTTP/2 다중화와 HTTP/3의 QUIC 전송은 요청이 연결을 공유하는 방식을 개선할 수 있습니다. 효과는 연결, 네트워크, 서버 구현에 따라 달라집니다. 어느 프로토콜도 느린 데이터베이스 작업을 없애거나 모든 요청의 첫 바이트 단축을 보장하지 않습니다.

연결 재사용과 TLS 세션 재개는 적절한 조건에서 반복 설정 작업을 줄입니다. 서버 설정을 바꾸기 전에 호스팅과 CDN이 이미 제공하는 기능을 확인합니다. 브라우저-CDN 연결과 CDN-원본 연결은 서로 다른 프로토콜을 사용할 수 있습니다.

HTTP 103 Early Hints는 지원 클라이언트가 최종 응답 전에 중요한 리소스 작업을 시작하게 합니다. 최종 HTML 생성이 즉시 끝나는 것은 아니며 브라우저와 중간 경로의 지원에 달려 있습니다. 확인된 발견 지연에 적용하고 힌트가 없어도 작동하는 기본 경로를 유지합니다.

Early Hints는 첫 바이트 측정의 의미도 바꿀 수 있습니다. 현재 Chrome은 중간 응답을 responseStart에 포함합니다. 지원되는 경우 finalResponseHeadersStart는 최종 응답 헤더의 시작을 나타냅니다. 이런 구성을 비교할 때 브라우저와 타이밍 필드를 기록합니다. 중간 응답이 빨라졌다고 최종 HTML이 빨리 도착했다고 입증되는 것은 아닙니다.

압축은 주로 전송 바이트를 줄입니다. 실시간 압축은 서버 CPU도 사용하므로 작은 출력이 항상 첫 바이트 지연 감소를 뜻한다고 주장하지 말고 효과를 측정합니다. HTTP/2와 HTTP/3 설명으로 전송 개선과 애플리케이션 개선을 구분할 수 있습니다.

호스팅이 병목인지 판단합니다

응답 지연이 worker 고갈, CPU 포화, 메모리 압박, 저장 장치 지연, 데이터베이스 경합과 함께 발생하면 인프라를 조사합니다. 혼잡 시간과의 상관관계는 단서지만 상위 요금제를 선택하기 전에 제한된 자원을 식별해야 합니다.

비슷한 전달 경로에서 단순 정적 응답과 대표적인 비캐시 애플리케이션 응답을 비교합니다. 애플리케이션만 느리면 의존성과 실행을 살펴봅니다. 둘 다 부하에서 나빠지면 공유 자원과 호스팅 경로를 확인합니다.

공유 호스팅에서는 모든 지표를 볼 수 없을 수 있습니다. 시간, 영향받는 URL, 캐시 상태, 응답 헤더, 반복 측정값을 공급자에게 제시합니다. 어느 한도에 도달했는지, 제안한 해결에 필요한 설정을 요금제에서 사용할 수 있는지 묻습니다.

처음 실패했던 경로에서 개선을 검증합니다

관련 구성요소 하나를 바꾼 뒤 원래 느렸던 시나리오를 반복합니다. 시간뿐 아니라 상태 코드, 콘텐츠 정확성, 세션 분리, 무효화, 오류 처리도 확인합니다. 작업을 뒤로 옮기기만 한 것이 아니라 콘텐츠 렌더링도 개선되었는지 확인합니다.

활성화되어 있다면 PageSpeed 분석 도구로 공개 URL을 점검하고 실험실 조건과 사용 가능한 현장 데이터를 보관합니다. 다음 병목이 확인되면 서버 성능 가이드로 이어갑니다. 목표는 의도한 방문자에게 올바른 콘텐츠를 유지하면서 반복적으로 더 빠른 응답을 제공하는 것입니다.

관련 가이드