SSD Nodes Learn 🎉 VPS $5.50/월부터
가이드 Matt Connor작성자 Matt Connor · 업데이트됨 2026-08-13

HTTP란 무엇인가? 서버 관리자를 위한 핵심 프로토콜 가이드

서버 관리자가 반드시 알아야 할 HTTP의 동작 원리를 설명합니다. 요청 메서드와 상태 코드, 헤더의 역할, nginx 로그 분석 방법, 그리고 HTTP/3와 TLS가 서버 설정에 미치는 영향을 실무 관점에서 정리했습니다.

HTTP란 무엇인가?

HTTP(hypertext transfer protocol)는 클라이언트와 웹 서버가 정보를 요청하고 응답을 주고받기 위해 사용하는 규칙의 집합입니다. 클라이언트는 요청을 보냅니다. 여기에는 GET와 같은 메서드, /pricing과 같은 경로, 프로토콜 버전, 헤더 목록, 그리고 때에 따라 본문(body)이 포함됩니다. 서버는 200과 같은 상태 코드와 함께 자체 헤더, 그리고 일반적으로 본문을 포함하여 응답합니다. 서버에서 발생하는 모든 페이지 조회와 모든 API(application programming interface) 호출은 이러한 교환 과정이 반복되는 것입니다.

HTTP는 자체적으로 상태를 유지하지 않습니다. 서버는 방금 전 사용자가 무엇을 요청했는지 기억하지 못합니다. 따라서 로그인 세션과 같이 메모리처럼 동작해야 하는 모든 정보는 모든 요청의 헤더에 담겨 전달됩니다. 이러한 특성은 이어지는 내용의 많은 부분을 설명합니다. 캐싱은 전적으로 헤더에 의해 제어되며, 로드 밸런서는 서비스에 영향을 주지 않고 다음 요청을 다른 백엔드로 보낼 수 있습니다.

아래의 모든 내용은 서버 측면에서, 즉 접근 로그와 nginx 설정에서 해당 모델이 어떻게 나타나는지를 다룹니다.

주석이 포함된 원시 요청 및 응답

다음은 완전한 HTTP/1.1 요청입니다. 빈 줄이 헤더의 끝을 나타내며, 그 이후의 모든 내용은 본문(body)입니다. GET는 일반적으로 본문을 포함하지 않습니다.

GET /pricing HTTP/1.1
Host: example.com
User-Agent: curl/8.5.0
Accept: */*
Accept-Encoding: gzip
  • GET은 수행할 작업을 나타내는 메서드입니다. GET는 읽기, POST는 데이터 전송, PUT은 교체, DELETE은 삭제, HEAD은 본문을 제외한 GET의 헤더만 요청합니다.
  • /pricing은 경로입니다. 호스트 이름은 요청 라인에 포함되지 않으므로 다음 헤더가 존재합니다.
  • HTTP/1.1은 클라이언트가 사용하는 프로토콜 버전입니다.
  • Host: example.com는 클라이언트가 원하는 사이트의 이름입니다. HTTP/1.1에서는 필수 항목이므로, nginx는 이 헤더가 없는 요청에 대해 400 Bad Request으로 응답합니다.
  • 나머지는 환경 설정입니다. Accept-Encoding: gzip는 클라이언트가 압축 해제를 지원함을 나타내며, 따라서 서버는 본문을 압축하여 전송할 수 있습니다.

응답도 상단에 상태 라인이 있다는 점을 제외하면 동일한 형태를 가집니다.

HTTP/1.1 200 OK
Date: Thu, 06 Aug 2026 09:12:44 GMT
Server: nginx
Content-Type: text/html; charset=utf-8
Content-Length: 5310
Cache-Control: public, max-age=300

<!doctype html>...
  • 200 OK는 상태 코드와 그에 대한 이유 구문입니다. 중요한 것은 코드이며, 구문은 장식일 뿐이므로 클라이언트는 이를 무시합니다.
  • Content-Type은 클라이언트가 이어지는 바이트를 어떻게 처리해야 하는지 알려줍니다.
  • Content-Length은 본문의 크기를 바이트 단위로 나타내며, 이를 통해 클라이언트는 본문의 끝을 알 수 있습니다. 크기를 미리 알 수 없는 경우 서버는 대신 Transfer-Encoding: chunked을 보내고 길이가 0인 청크로 끝을 표시합니다.
  • Cache-Control는 브라우저와 중간 캐시가 이 응답을 얼마나 오래 보관할 수 있는지 알려줍니다.
  • 헤더 뒤의 빈 줄은 요청과 응답 양방향 모두에서 헤더와 본문을 구분합니다.

헤더 이름은 대소문자를 구분하지 않으며, 모든 라인은 단순 줄바꿈이 아닌 캐리지 리턴과 라인 피드로 끝납니다. 이를 직접 입력할 일은 없겠지만, 패킷 캡처를 통해 확인할 수 있습니다.

실제 요청과 응답 쌍을 확인하려면 본인이 소유한 사이트를 대상으로 다음 명령을 실행하십시오.

curl -sS -o /dev/null -D - https://example.com/

-D -은 응답 헤더를 터미널에 출력하고 -o /dev/null은 본문을 폐기합니다. curl -I보다는 이 방식을 권장하는데, -IHEAD 요청을 보내기 때문입니다. HEADGET을 다르게 처리하는 애플리케이션 서버(실제로 많은 서버가 그렇습니다)는 브라우저가 절대 받지 못할 헤더를 보여줄 수 있습니다. curl -v은 요청 라인을 >로, 응답 라인을 <로 표시하여 양쪽 모두를 출력합니다.

Nginx 접근 로그의 요청 라인 구성

Nginx는 combined 로그 형식을 제공하며, 그 정의는 다음과 같습니다.

log_format combined '$remote_addr - $remote_user [$time_local] '
                    '"$request" $status $body_bytes_sent '
                    '"$http_referer" "$http_user_agent"';

이 형식으로 생성된 로그 한 줄의 예시입니다.

203.0.113.45 - - [06/Aug/2026:09:12:44 +0000] "GET /pricing HTTP/1.1" 200 5310 "https://example.com/" "Mozilla/5.0 (X11; Linux x86_64) Chrome/127.0.0.0 Safari/537.36"
  • 203.0.113.45$remote_addr이며, TCP(transmission control protocol) 연결을 맺은 주소입니다. 프록시 뒤에 있을 경우 이 값은 방문자가 아닌 프록시의 주소입니다.
  • 첫 번째 -은 고정된 자리 표시자입니다. 두 번째는 $remote_user이며, HTTP 기본 인증을 사용할 때만 값이 채워집니다.
  • "GET /pricing HTTP/1.1"$request으로, 서버에 도착한 요청 라인을 그대로 복사한 것입니다.
  • 200은 서버가 반환한 상태 코드이며, 방문자가 인식한 상태와는 다를 수 있습니다.
  • 5310$body_bytes_sent로, 응답 본문의 크기만을 나타냅니다. 응답 헤더는 포함되지 않으므로 이 숫자는 실제 전송된 바이트 수보다 항상 작습니다.
  • 마지막 두 개의 따옴표로 묶인 필드는 RefererUser-Agent입니다. 두 값 모두 클라이언트가 제공하므로 어떤 내용이든 포함될 수 있습니다.

$request는 그대로 복사되기 때문에, 잘못된 데이터도 그대로 기록됩니다. 일반 텍스트 포트 80으로 TLS(transport layer security) 통신을 시도하는 클라이언트는 400 라인을 남기며, 요청 필드는 "\x16\x03\x01\x02\x00\x01"와 같은 이스케이프된 바이트로 시작합니다. \x16는 TLS 핸드셰이크 레코드 유형이므로, 해당 바이트들은 요청 라인이 아니라 ClientHello의 시작 부분입니다. 서버는 정상적으로 동작하고 있는 것입니다. 누군가 HTTPS 요청을 HTTP 포트로 보내고 있습니다.

로그 형식에 $server_protocol도 추가하십시오. 이 항목은 HTTP/1.1, HTTP/2.0 또는 HTTP/3.0을 출력하며, 프로토콜 변경이 실제로 적용되었는지 확인하는 가장 빠른 방법입니다.

자체 사이트에서 반환하는 일반적인 상태 코드의 의미

첫 번째 숫자는 클래스를 나타내며, 가장 먼저 확인해야 할 부분입니다.

2xx는 성공을 의미합니다. 200 OK은 일반적인 읽기 요청에 대한 응답입니다. 201 Created는 무언가를 생성한 POST 요청 이후에 반환됩니다. 204 No Content는 반환할 본문이 없는 성공을 의미하며, 이는 보통 DELETE 요청에 대한 일반적인 응답입니다.

3xx는 다른 곳을 확인하라는 의미입니다. 301은 영구적인 이동을 의미하며 브라우저가 강력하게 캐싱합니다. 때로는 사용자가 프로필을 삭제할 때까지 유지되므로, 잘못된 호스트네임을 가리키는 301은 되돌리기 매우 어렵습니다. 리다이렉트를 테스트하는 동안에는 302을 사용하십시오. 304 Not Modified는 오류가 아닌 성공입니다. 클라이언트가 여전히 인식 가능한 ETag(엔티티 태그)을 포함한 If-None-Match을 보냈으므로, 본문 없이 헤더만 응답한 것입니다. 로그에 304가 가득하다면 캐싱이 정상적으로 작동하고 있다는 뜻입니다.

4xx는 요청이 잘못되었음을 의미합니다. 400 Bad Request은 입력 형식이 잘못되었음을 나타냅니다. 401 Unauthorized는 인증되지 않았음을 의미하며, 반드시 인증 방식을 명시하는 WWW-Authenticate 헤더를 포함해야 합니다. 403 Forbidden은 요청을 이해했지만 거부했음을 의미합니다. 404 Not Found은 존재하지 않는 경로입니다. 405 Method Not Allowed은 경로는 맞지만 메서드가 잘못된 경우이며, 정적 파일 위치에 POST 요청을 보낼 때 반환됩니다. 413은 본문 크기가 nginx의 client_max_body_size 설정(기본값 1메가바이트)보다 클 때 발생하며, 에러 로그에 client intended to send too large body로 기록됩니다.

정적 파일에서 발생하는 403은 거의 항상 HTTP 규칙이 아닌 파일 시스템 문제입니다. 설정을 변경하기 전에 /var/log/nginx/error.log를 읽어보십시오. open() "/srv/site/index.html" failed (13: Permission denied)는 nginx 워커 사용자가 파일을 읽을 수 없음을 의미하며, 주로 상위 디렉터리에 others에 대한 실행 권한이 없을 때 발생합니다. directory index of "/srv/site/" is forbidden은 경로가 인덱스 파일이 없는 디렉터리로 해석되었고 autoindex이 꺼져 있을 때 발생합니다.

5xx는 서버 측의 오류를 의미합니다. 500은 애플리케이션에서 처리되지 않은 오류입니다. 502 Bad Gateway는 nginx가 업스트림으로부터 유효한 응답을 받지 못했음을 의미하며, 에러 로그에 원인이 기록됩니다. connect() failed (111: Connection refused) while connecting to upstreamproxy_pass 주소에서 수신 대기 중인 서비스가 없음을 의미합니다. 504 Gateway Timeout는 업스트림이 연결은 수락했으나 proxy_read_timeout(기본값 60초) 동안 아무런 응답이 없었음을 의미하며, 로그에는 upstream timed out (110: Connection timed out) while reading response header from upstream로 기록됩니다. 503 Service Unavailable는 의도적인 연결 거부입니다. nginx 자체의 속도 제한기는 503을 반환하는데, 이는 limit_req_status의 기본값이 503이기 때문입니다. 로그에서 429 Too Many Requests를 찾고 있는데 503이 발견된다면 바로 이 때문입니다. 정확한 코드를 확인하려면 limit_req_status 429;을 설정하십시오.

서버 운영 시 중요한 헤더

Host는 사이트를 선택합니다. 하나의 IP 주소로 수백 개의 호스트 이름을 서비스할 수 있으며, nginx는 Hostserver_name을 대조하여 어떤 server 블록이 응답할지 결정합니다. 일치하는 항목이 없으면 nginx는 기본 서버를 사용합니다. 이는 별도로 default_server로 표시된 블록이 없다면 해당 주소와 포트에서 수신 대기하는 첫 번째 블록입니다. 새로운 가상 호스트에서 의도하지 않은 사이트가 응답한다면 거의 항상 이 문제입니다. 이름이 일치하지 않아 요청이 기본 서버로 넘어간 것입니다. DNS를 건드리지 않고 다음 명령으로 테스트하십시오.

curl -sS -o /dev/null -D - -H 'Host: app.example.com' http://127.0.0.1/

User-Agent는 클라이언트가 작성한 자기 설명이며 자유 형식 텍스트입니다. 로그를 읽을 때 힌트로 사용하십시오. 이를 제어 수단으로 사용해서는 안 됩니다. 거짓 정보를 보내려는 클라이언트는 단순히 그렇게 하기 때문입니다. 따라서 User-Agent으로 스크래퍼를 차단하는 것은 예의 바른 스크래퍼만 걸러낼 뿐입니다.

Content-Type은 바이트를 해석하는 방법을 결정합니다. API 요청에는 application/json을, 페이지에는 text/html; charset=utf-8을 사용합니다. nginx는 /etc/nginx/mime.types를 통해 파일 확장자를 타입에 매핑하며, 패키지에 포함된 nginx.confdefault_type application/octet-stream;을 설정합니다. 따라서 nginx가 알지 못하는 확장자를 가진 파일은 렌더링되지 않고 다운로드로 제공됩니다. 눈에 보이는 증상은 브라우저 콘솔에 Refused to apply style from ... because its MIME type ('text/plain') is not a supported stylesheet MIME type가 출력되면서 스타일 없이 페이지가 로드되는 것입니다. 여기서 MIME은 다목적 인터넷 메일 확장(Multipurpose Internet Mail Extensions)의 약자로, 해당 타입 문자열이 유래한 명명 체계입니다.

Cache-Control은 서버와 독자 사이의 모든 캐시를 제어하는 방법입니다. public, max-age=31536000, immutable은 파일 이름에 콘텐츠 해시가 포함된 에셋에 적합합니다. 콘텐츠가 변경되면 이름도 바뀌기 때문입니다. no-store는 사용자별 데이터에 사용해야 합니다. 공유 캐시가 로그인된 페이지를 보관하면 동일한 URL을 요청하는 다음 사람에게 해당 페이지를 전달할 수 있기 때문입니다. private는 중간 설정입니다. 브라우저는 보관할 수 있지만, 공유 캐시는 보관할 수 없습니다.

X-Forwarded-For는 프록시가 방문자를 숨기기 때문에 존재합니다. 요청이 리버스 프록시를 통과하면 $remote_addr은 프록시의 주소가 되므로, 로그, 지리적 위치 정보, 속도 제한(rate limiting) 기능은 모두 하나의 클라이언트만 보게 됩니다. 프록시는 원래 주소를 전달해야 합니다.

proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;

수신 서버는 해당 정보를 신뢰하도록 설정되어야 하며, 정확히 누구를 신뢰할지 지정해야 합니다.

set_real_ip_from 10.0.0.0/8;
real_ip_header X-Forwarded-For;

직접 관리하는 범위만 나열하십시오. X-Forwarded-For은 모든 클라이언트가 보낼 수 있는 일반 텍스트이므로, set_real_ip_from 0.0.0.0/0;을 사용하면 방문자가 로그에 기록될 주소와 속도 제한기가 계산할 주소를 임의로 선택할 수 있게 됩니다.

X-Forwarded-Proto는 매우 흔하고 구체적인 오류를 방지합니다. 프록시가 TLS를 종료하고 일반 HTTP를 통해 애플리케이션으로 요청을 전달합니다. 애플리케이션은 일반 요청을 확인하고 방문자가 HTTPS를 사용해야 한다고 판단하여 301 https://example.com/로 응답합니다. 브라우저가 이를 따르면 프록시는 다시 TLS를 종료하고 일반 HTTP를 전달하며, 브라우저가 ERR_TOO_MANY_REDIRECTS 오류를 낼 때까지 이 루프가 반복됩니다. X-Forwarded-Proto: https을 보내면 애플리케이션에 방문자가 이미 HTTPS를 사용 중임을 알리므로 리다이렉트를 중단합니다.

HTTP/1.1 대 HTTP/2 대 HTTP/3: 무엇이 달라지는가

HTTP/1.1은 텍스트 기반이며 연결당 한 번에 하나의 요청만 처리합니다. Connection: keep-alive를 사용하면 다음 요청이 동일한 TCP 연결을 재사용할 수 있어 설정 비용을 절감하지만, 응답은 여전히 요청된 순서대로 돌아옵니다. 하나의 느린 응답이 뒤에 대기 중인 모든 요청을 차단합니다. 이를 HOL(Head-of-Line) 블로킹이라 하며, 브라우저는 동일한 호스트 이름에 여러 연결을 동시에 열어 이를 우회합니다.

HTTP/2는 동일한 메서드와 상태 코드를 유지하면서 프레임 구조를 바이너리로 변경합니다. 여러 요청이 하나의 연결을 독립적인 스트림으로 공유하며, 반복되는 헤더 텍스트는 압축됩니다. 현대의 요청은 많은 헤더를 포함하므로 이는 중요합니다. 연결은 여전히 TCP이므로, 패킷 하나가 손실되면 재전송이 도착할 때까지 해당 연결의 모든 스트림이 멈춥니다. HOL 블로킹은 사라지지 않았습니다. HTTP 계층에서 전송 계층으로 내려갔을 뿐입니다. 서버 푸시(Server push)는 HTTP/2의 일부였으나, 2022년 Chrome이 지원을 중단하면서 사실상 사라졌습니다.

HTTP/3는 다시 동일한 의미론을 유지하면서 TCP를 UDP(User Datagram Protocol) 기반의 전송 프로토콜인 QUIC으로 대체합니다. QUIC 스트림은 완전히 독립적이므로, 패킷이 손실되어도 해당 패킷이 속한 스트림만 멈춥니다. TLS 1.3은 계층 위에 얹히는 대신 QUIC 핸드셰이크에 내장되어 있어, 새로운 연결에 필요한 왕복 횟수가 줄어듭니다. 여기에는 두 가지 실질적인 결과가 따릅니다. 경로상의 모든 방화벽에서 UDP 443 포트가 열려 있어야 하며, UDP를 제한하거나 차단하는 네트워크는 클라이언트를 HTTP/2로 되돌립니다.

구체적으로 무엇이 달라지는지 확인하십시오. 브라우저는 처음부터 HTTP/3로 시작하지 않습니다. HTTP/2나 HTTP/1.1로 연결한 뒤 응답에서 Alt-Svc: h3=":443"; ma=86400 헤더를 확인하고, 이후 해당 호스트에 대한 연결부터 HTTP/3를 사용합니다. 따라서 이 헤더는 선택적인 장식이 아닙니다. 이는 발견 메커니즘입니다. nginx의 경우, 버전 1.25.1부터 HTTP/2가 별도의 지시어로 분리되었습니다(기존 listen ... http2 매개변수를 대체하여 server 블록 내에 http2 on; 사용). QUIC은 메인라인 1.25.0부터 도입되었으며, HTTP/3 사이트를 운영하려면 일반 listen 443 ssl;과 함께 listen 443 quic reuseport; 설정이 필요합니다.

프록시마다 성숙도가 다르므로 현재 실행 중인 버전을 확인해야 합니다. 2026년 8월 기준으로 Caddy는 별도의 설정 없이 기본적으로 HTTP/3를 제공합니다. nginx는 명시적인 quic 리스너와 위에서 설명한 Alt-Svc 헤더가 필요합니다. Traefik은 명시적인 http3 옵션을 통해 진입점별로 이를 활성화합니다. 여러 Docker 앱 앞단의 Traefik에서 TLS를 종료하는 경우, 방문자가 사용하는 프로토콜 버전은 여기서 결정되며, 프록시에서 컨테이너로 가는 구간은 브라우저와의 협상 결과와 관계없이 보통 일반 HTTP/1.1을 사용합니다.

추측하지 말고 검증하십시오. curl --http3 -sS -o /dev/null -D - https://example.com/curl -V이 기능 목록에 HTTP3를 포함하는 경우에만 작동하며, 대부분의 배포판 빌드에는 포함되어 있지 않습니다. 가장 확실한 확인 방법은 로그를 보는 것입니다. 로그 형식에 $server_protocol를 추가하여 실제 브라우저가 무엇을 협상하는지 읽으십시오. 그전에 UDP 443 포트가 실제로 열려 있는지 확인하십시오. TCP 443만 허용하는 방화벽은 사이트가 HTTP/2로 계속 작동하는 동안 HTTP/3를 조용히 실패하게 만듭니다. Linux 서버에서 열려 있고 수신 대기 중인 포트 확인하기는 가장 먼저 수행해야 할 작업입니다.

HTTPS: HTTP는 프로토콜이고 TLS는 래퍼입니다

HTTPS는 별도의 프로토콜이 아닙니다. TLS 세션 내부에서 동일한 요청과 상태 코드를 주고받는 방식입니다. 80번 포트는 평문으로 통신하고 443번 포트는 암호화하여 통신합니다. TLS 핸드셰이크가 먼저 완료된 후, 암호화된 채널을 통해 HTTP 요청이 전달됩니다. 이러한 순서 때문에 인증서 문제는 상태 코드가 할당되지 않습니다. HTTP 바이트가 전송되기 전에 실패가 발생하므로 응답 번호를 부여할 수 없습니다.

여러 사이트를 호스팅하는 서버에서는 순서가 중요합니다. 인증서는 SNI(server name indication)를 사용하여 선택되는데, 이는 HTTP 헤더가 존재하기 전 TLS 핸드셰이크 단계에서 호스트 이름을 평문으로 전달하는 필드입니다. 따라서 서버는 SNI를 통해 인증서를 먼저 선택하고, 그 다음 Host 헤더를 통해 가상 호스트를 선택합니다. 이 두 번의 조회는 일반적으로 일치해야 합니다. 일치하지 않으면 브라우저는 NET::ERR_CERT_COMMON_NAME_INVALID과 같은 이름 불일치 오류를 표시하고 요청을 전혀 보내지 않습니다. 이는 기본 서버의 인증서가 해당 이름에 유효하지 않기 때문입니다.

공개 사이트라면 정식 인증서를 발급받고 자동으로 갱신되도록 설정하십시오. nginx에서 Let's Encrypt를 사용하는 Certbot은 서버 블록에 인증서 경로를 기록하고 갱신 타이머를 자동으로 설치합니다. 내부 이름이나 사설 네트워크의 IP 주소와 같이 공인 기관에서 검증할 수 없는 호스트 이름의 경우, Ubuntu에서 자체 서명 인증서 생성이 정직한 선택입니다. 단, 모든 클라이언트가 해당 인증서를 신뢰하도록 설정해야 함을 인지해야 합니다.

TLS 설정이 완료되면 80번 포트로 들어오는 모든 요청을 443번 포트로 리다이렉트하십시오.

server {
    listen 80;
    server_name example.com;
    return 301 https://$host$request_uri;
}

Strict-Transport-Security는 확실할 때만 추가하십시오. add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always; 헤더는 브라우저가 해당 호스트 이름에 대해 2년 동안 평문 HTTP 접속을 거부하도록 지시합니다. 브라우저는 이를 자체 캐시에 저장하므로, 나중에 헤더를 제거해도 설정이 되돌려지지 않습니다. 처음에는 max-age을 몇 시간 정도로 짧게 설정하고, 모든 서브도메인이 HTTPS에서 정상 작동하는지 확인한 뒤에 값을 높이십시오.

FAQ

HTTP와 HTTPS의 차이점은 무엇입니까?

HTTPS는 TLS(Transport Layer Security) 세션 내부에서 전송되는 HTTP입니다. 메서드와 상태 코드는 동일합니다. 차이점은 클라이언트와 TLS 종료 지점 사이에서 바이트가 암호화된다는 점과 기본 포트가 80에서 443으로 바뀐다는 점입니다. TLS 핸드셰이크는 첫 번째 HTTP 바이트가 전송되기 전에 완료되므로, 인증서 오류는 상태 코드를 생성하지 않습니다. 이것이 브라우저 인증서 경고에 403과 같은 숫자 대신 NET::ERR_CERT_COMMON_NAME_INVALID과 같은 오류 이름이 표시되는 이유입니다.

사이트에서 502 Bad Gateway가 발생하는 이유는 무엇입니까?

nginx에서 발생하는 502는 nginx가 프록시 대상인 업스트림으로부터 유효한 응답을 받지 못했음을 의미합니다. 즉, 방문자의 요청은 정상이었으나 nginx 뒤의 서비스에 문제가 있는 것입니다. /var/log/nginx/error.log을 읽어 보십시오. connect() failed (111: Connection refused) while connecting to upstreamproxy_pass에 지정된 주소와 포트에서 수신 대기 중인 서비스가 없음을 의미하므로, 애플리케이션이 실행 중인지, 예상한 주소에 바인딩되었는지 확인하십시오. no live upstreams while connecting to upstream은 반복된 실패로 인해 업스트림 블록의 모든 서버가 다운 상태로 표시되었음을 의미합니다. 연결은 수락했으나 proxy_read_timeout 이내에 응답하지 못한 경우인 504 Gateway Timeout과 비교해 보십시오.

액세스 로그에 모든 방문자의 IP 주소가 동일하게 표시되는 이유는 무엇입니까?

$remote_addr는 TCP 연결을 맺은 주소를 기록하는데, 리버스 프록시나 콘텐츠 전송 네트워크(CDN) 뒤에서는 해당 주소가 프록시의 주소이기 때문입니다. 방문자의 실제 주소는 X-Forwarded-For 헤더에 담겨 전달됩니다. 프록시에서 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;을 설정한 다음, 수신 측 nginx에서 set_real_ip_from를 프록시의 주소 대역으로 설정하고 real_ip_header X-Forwarded-For;을 적용하십시오. 해당 헤더는 모든 클라이언트가 보낼 수 있는 텍스트이므로, 전체 인터넷에 대해 신뢰할 경우 방문자가 로그에 남을 주소와 속도 제한(rate limit) 대상 주소를 임의로 선택할 수 있게 됩니다. 따라서 직접 관리하는 주소 대역만 나열해야 합니다.

HTTP/2나 HTTP/3를 활성화해야 합니까?

HTTP/2는 활성화할 가치가 있습니다. 이미 TLS가 적용된 사이트라면 설정 한 줄로 적용할 수 있으며, 연결당 요청 제한을 제거하여 작은 파일이 많은 페이지의 로딩 속도를 개선하기 때문입니다. HTTP/3는 얻을 수 있는 이득이 작고 불확실하며, UDP 443 포트를 열어야 하고 QUIC을 지원하는 프록시 빌드가 필요합니다. 브라우저는 이전 응답에서 Alt-Svc 헤더를 확인한 후에만 HTTP/3로 전환한다는 점을 기억하십시오. 따라서 해당 헤더가 없다면 listen 설정과 관계없이 아무것도 바뀌지 않습니다. 로그 형식에 $server_protocol을 추가하여 시간을 투자하기 전에 방문자가 실제로 어떤 프로토콜을 협상하는지 측정해 보십시오.

파일이 존재하는데 403 Forbidden이 발생하는 이유는 무엇입니까?

정적 사이트에서 403은 대개 HTTP 규칙보다는 파일 시스템 권한 문제인 경우가 많습니다. /var/log/nginx/error.log 내의 open() ... failed (13: Permission denied)은 nginx 워커 프로세스 사용자가 파일을 읽을 수 없음을 의미합니다. 이는 파일 자체의 모드 문제보다는 상위 디렉터리에 others에 대한 실행 권한이 없는 경우가 대부분입니다. directory index of ... is forbidden은 요청이 인덱스 파일이 없는 디렉터리로 연결되었으나 autoindex이 꺼져 있을 때 발생합니다. 일치하는 location 블록에 명시적인 deny 규칙이 있어도 403가 반환될 수 있으므로, 오류 로그에 아무 내용이 없다면 해당 블록을 확인하십시오.

#http#https#web-server#headers#http3