WordPress TTFB 단축을 위한 작업 3차

You are currently viewing WordPress TTFB 단축을 위한 작업 3차
Generated by Gemini

이전 게시글 보기 (WordPress TTFB 단축을 위한 작업 1차)

이전 게시글 보기 (WordPress TTFB 단축을 위한 작업 2차)

 

 

WordPress 를 쓸 땐 과거에 PHP 환경에 맞춰서 처리하기 위해 가장 간단한 웹서버인 Apache 를 기반으로 운영을 했었다.

하지만 서버를 운영하면 할 수 록 Apache 에 대한 한계가 명확하게 드러나는 문제가 여전히 발생할 수 밖에 없었다.

따라서 후속작업으로 여러번의 삽질을 하며 서버의 웹 사이트 속도를 끌어올리는데 주목했다.

 

Apache2 에서 Nginx 로 전환

사실 Apache2 에서 Nginx 로 전환하는데 가장 큰 걸림돌은 Apache2 에서만 가능한 .htaccess 의 사용여부였다.

이게 Apache 가 웹 사이트를 읽고 처리하기전에 .htaccess 의 룰에 따라 자동으로 처리되도록 하는 이 기능이 Nginx 에서는 제한적이거나 불가능한 케이스도 있어서 명확한 장단점이 있었기 때문이다.

특히 마이그레이션을 하는거 자체는 굉장히 위험한 도전이기에 쉽게 작업을 하지 못했던 점도 있었다.

 

하지만, 속도 개선을 위해서 어느정도 하드웨어 투자도 했다면 이제는 소프트웨어의 작업도 필요하다 판단되어 작업을 진행하게 되었다.

 

 

Nginx.conf

user nginx; # Nginx Worker Process 가 실행될 사용자
worker_processes auto; # CPU Core 수 에 맞게 자동 설정

pid /run/nginx.pid;
include /etc/nginx/modules-enable/*.conf;

# brotli 모듈
load_module modules/ngx_http_brotli_filter_module.so;
load_module modules/ngx_http_brotli_static_module.so;

events {
    worker_connections 25565; # Nginx Worker Process 가 동시에 처리할 수 있는 최대 클라이언트 연결 수
    multi_accept on; # Nginx worker Process 가 한번에 여러개의 새로운 연결을 받아들일지 여부
    use epoll; # epoll 을 사용함. epoll 은 select 의 단점을 보완한 I/O 시스템 호출임.
}

http { # Global 설정
    charset utf-8; # UTF-8 설정
    sendfile on; # Nginx 가 파일을 직접 읽어 Network Socket 으로 쓸지, 운영체제의 sendfile() System Call 을 쓸건지 여부 (on : System Call / off : Nginx)
    tcp_nopush on; # Nginx 가 TCP_CORK 옵션을 Socket 적용하도록 지시할지 여부
    tcp_nodelay on; # Nginx 가 TCP Socket 에 데이터를 즉시 보낼지(TCP_NODELAY 옵션) 여부
    keepalive_timeout 60;
    types_hash_max_size 2048; # Nginx 가 MIME 타입을 찾을 때 사용하는 Hash 테이블의 최대 크키 설정

    proxy_buffers 32 128k; # an upstream response is buffered to a temporary file 오류 발생 방지용 프록시 버퍼크기 조절 / 16개의 버퍼를 사용, 각 버퍼의 크기는 16k 를 의미.
    proxy_buffer_size 128k; # Nginx 가 Client 요청을 다른 BackEnd Server (Apache2 등 Proxy 사용) 로 전달하고 그 BackEnd Server 로 부터 Response 를 받을 때 사용하는 Buffer 의 크기를 설정
    proxy_busy_buffers_size 128k; # Nginx 가 아직 데이터를 Client 에 전송 중이지만 아직 전송이 끝나지 않은 경우에 대한 Buffer 의 최대 크기
    client_header_buffer_size 1k; # Nginx 가 Client 로 부터 HTTP 요청 헤더를 읽어들일 때 사용하는 버퍼의 크기를 설정
    client_body_buffer_size 128k; # Nginx 가 Client 로 부터 HTTP 요청 본문을 읽어들일 때 사용하는 버퍼의 크기를 설정
    large_client_header_buffers 4 4k; # Nginx 가 큰 헤더를 가질 때 전용의 Buffer 크기 설정

    # 2025.06.25 - 보안설정
    server_tokens off; # Nginx 의 버전을 숨김
    autoindex off; # 디렉토리 리스닝 방지
    disable_symlinks on; # 심볼릭 링크 방지

    # 클라이언트가 slow connection 하는것을 방지
    client_body_timeout 10s; # Client 가 요청 후 Response Body 보내기 까지 기다리는 시간
    client_header_timeout 10s; # Client 가 요청 후 Response Header 보내기 까지 기다리는 시간

    include /etc/nginx/mime.types;
    error_log /var/log/nginx/error.log warn;

    # Global header 설정
    add_header X-Frame-Options "SAMEORIGIN";
    add_header X-Content-Type-Options "nosniff";
    add_header X-XSS-Protection "1; mode=block";
    add_header X-UA-COMPATIBLE "IE=edge,chrome=1";

    # TLS 설정
    ssl_session_cache shared:SSL:10m; # shared:SSL : SSL 이라는 이름으로 여러 Worker Process 간에 공유되는 캐시를 생성 / 10m : 10MB 정도로 Cache 를 생성. 약 40,000 여개의 Session 저장 가능
    ssl_session_timeout 10m; # Cache 정보를 얼마나 유효한 시간동안 설정할지에 대한 값
    ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3;
    ssl_prefer_server_ciphers on; # TLS Handshake 과정에서 사용할 Cipher Suite 를 결정할 때, 서버의 선호도를 우선할지 여부
    ssl_ciphers "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384"; # Nginx 가 어떤 Chiper Shite 를 설정할지에 대한 값
    # ssl_stapling off; # 서버가 클라이언트에게 TLS 인증서를 보낼 때 인증서가 유효한지 OCSP 응답을 함께 보낼지 여부 (Cloudflare 쓰므로 off)
    # ssl_stapling_verify off; # TLS 인증서가 유효한지 OCSP 서버로 보내 유효성 검사 여부 (Cloudflare 쓰므로 off)
    # resolver 1.1.1.1 1.0.0.1 valid=300s; # Nginx 가 DNS 를 IP로 변환할 때 사용할 DNS 서버의 주소를 설정하는 값. / valid : DNS 응답이 캐시될 시간을 설정
    # resolver_timeout 5s; DNS 응답을 해당 값동안 기다림

    # 2025.06.28 - WordPress 전용으로 Nginx FastCGI Cache 설정
    fastcgi_cache_path /dev/shm/fastcgi levels=1:2 keys_zone=WORDPRESS:100m inactive=60m;
    # fastcgi_cache_path [Nginx 캐시 저장할 폴더] [Cache 를 Split 해서 저장할 Depth of Directory 를 명시]* [Cache 키와 값을 저장하기 위한 영역]** [지정한 값동안 Access 되지 않은 Cache 는 제거] [Cache 의 최대 크기]***
    # * Depth of Directory 미지정 시 동일한 Depth / 1 : 1자리 Cache Value / 2 : Directory Split 되어 계층형 저장
    # ** 이름:최대공유메모리
    # *** max_size=1g 로 했는데 사용하지 않음.
    fastcgi_cache_key "$scheme$request_method$host$request_uri"; # Cache 파일에 대한 고유 키값을 어떻게 지정할 것인지?
    # fastcgi_cache_key
    # scheme : http 나 https 등
    # request_method : GET 나 POST 등
    # host : 도메인 주소
    # request_uri : 쿼리 문자열을 포함한 전체 URI
    fastcgi_cache_use_stale error timeout invalid_header updating http_500 http_503; # Backend 가 오류를 반환하거나, Timeout 되거나, 유효하지 않은 헤더거나, 500 이나 503 오류를 반환할 때 Nginx 가 캐시에서 오래된 컨텐츠를 제공하도록 지시
    fastcgi_cache_min_uses 1; # 응답은 최소 1번 요청한 이후에 Cache 되도록 함
    fastcgi_cache_lock on; # 동일한 캐시되지 않은 항목으로 여러 요청이 동시에 들어올 경우, 첫번째 요청만 Backend로 전송, 나머지는 캐시될 때 까지 대기
    fastcgi_ignore_headers Cache-Control Expires Set-Cookie; # 일부 조건에 맞는 헤더는 무시함


    # Cloudflare 프록시
    set_real_ip_from 103.21.244.0/22;
    set_real_ip_from 103.22.200.0/22;
    set_real_ip_from 103.31.4.0/22;
    set_real_ip_from 104.16.0.0/13;
    set_real_ip_from 104.24.0.0/14;
    set_real_ip_from 108.162.192.0/18;
    set_real_ip_from 131.0.72.0/22;
    set_real_ip_from 141.101.64.0/18;
    set_real_ip_from 162.158.0.0/15;
    set_real_ip_from 172.64.0.0/13;
    set_real_ip_from 173.245.48.0/20;
    set_real_ip_from 188.114.96.0/20;
    set_real_ip_from 190.93.240.0/20;
    set_real_ip_from 197.234.240.0/22;
    set_real_ip_from 198.41.128.0/17;
    set_real_ip_from 2400:cb00::/32;
    set_real_ip_from 2606:4700::/32;
    set_real_ip_from 2803:f800::/32;
    set_real_ip_from 2904:f400::/32;
    set_real_ip_from 2a06:98c0::/29;
    set_real_ip_from 2c0f:f248::/32;

    real_ip_header X-Forwarded-For;
    # real_ip_header CF-Connecting-IP;
    real_ip_recursive on; # Proxy 에서 실제 Client IP 를 가져온 다음에 더이상 Proxy Chain 을 보지 않게 함

    include /etc/nginx/conf.d/*.conf; # 추가적인 설정 파일
    include /etc/nginx/sites-enabled/*.conf; # 실제 가상 호스트 설정 파일
    include snippets/brotli.conf; # brotli 설정 파일
}

 

해당 값에 대한 설명은 옆에 추가적으로 작성해놓은 주석등을 참고하면 도움이 된다.

 

기본적으로 Ubuntu 환경에서 apt install nginx를 통해 설치한 Nginx 버전은 1.18.0 인데 보안 취약점이 있는 구버전이라 하는 수 없이 1.28.0 같은 Stable 한 버전으로 올리는 작업을 진행했다.

추가적으로 Google 의 brotli 를 적용하여 gzip 보다 효율이 좋은 압축 알고리즘을 적용하여 서버에서 파일을 줄 때 안정적으로 줄 수 있도록 수정했다.

brotli 관련 작업을 참조한 링크 : https://www.wsgvet.com/home/593


보안 설정을 위해 몇가지 서버에 작업을 진행했는데, 과거에 /var/www/ 하위에 특정 경로에 있던 폴더 파일에 웹 파일들을 넣고 이를 Symbolic Link 처리해서 썼는데, Nginx 에서는 그것이 보안에 문제가 있다고 하여 /var/www/ 쪽 대신에 아예 기존에 있던 웹 파일이 있던 경로로 DocumentRoot 를 구성했다.


글로벌한 Header 지정을 위해 Nginx.conf 에서 최상단에서 먼저 불러와서 값이 씌워지도록 처리했다.


TLS Cipher 는 이 사이트가 Cloudflare 를 사용하므로 Let’s Encrypt 같은 인증서 대신에 Cloudflare 의 Self Signing 인증서를 통해 Cloudflare 와 서버간의 통신시에 대한 보안 처리로 추가적인 TLS 인증서를 삽입했다.


fastcgi_cache_path설정을 통해 RAM Disk 에 Nginx FastCGI Cache 를 넣어 페이지의 속도를 매우 빠르게 Cache 할 수 있도록 하여 TTFB 를 낮추는데 도움을 주었다.

특정 디렉토리를 생성 후, /etc/fstab 을 열어 Mount 할 형태를 설정하여 RAM Disk 를 생성하는 작업을 먼저 수행한다.

FastCGI 관련 작업을 참조한 링크 : https://happist.com/557860/워드프레스-최적화-fastcgi-cache-적용-워드프레스-반응-속도


Cloudflare 와 Nginx 간 연결을 다이렉트로 하는 경우 Cloudflare 에 의해 Proxy 된 IP 가 $remote_addr로 들어가는 특성상 실제 사용자의 접속 IP를 볼 수 없던 문제가 있어real_ip_header라는 옵션을 넣어 Cloudflare IP 대역으로 오는 경우에는 X-Forwarded-For 로 받아오게 했다. CF-Connecting-IP 로 넣는게 좋다고 하는데 일단은 전자것을 사용했다.

log_formats.conf

이것은서버에서 nginx 의 access.log 를 보기위해 테스트 하는 용도로 사용되고 있는 형태이다.

log_format combined_vhost '$http_x_forwarded_for - $remote_user [$time_local] '
                            '"$request" $status $body_bytes_sent '
                            '"$http_referer" "$http_user_agent" "$remote_addr" '
                            '$host'; # 어떤 도메인으로 호출했는지?

server.conf

실제 블로그가 동작되는 conf 파일의 정보다.

실제 운영되는 서버에 대한 Config 값이라 일부 정보는 변형되거나 검열된 부분이 있으므로 참고하면 좋다.

server { # IP 접근 방지
    # 대표로 하나만 지정해도 모든 nginx 에 동작하므로 중복 설정 필요없음
    # 중복 설정시 nginx: [emerg] a duplicate default server for 0.0.0.0:443 in /etc/nginx/sites-enabled/server.conf:2 오류 발생
    listen 443 ssl default_server; 
    server_name _;
    ssl_certificate /etc/apache2/cert/supersu-kr-cloudflare.pem;
    ssl_certificate_key /etc/apache2/cert/supersu-kr-cloudflare.key;
    return 444;
}

server {
    listen 443 ssl; # 443 포트 / TLS 사용
    listen [::]:443 ssl; # IPv6 도 허용
    http2 on; # http2 사용

    server_name supersu.kr www.supersu.kr; # 도메인 설정
    root [Censored]; # Document Root

    # TLS 인증서
    ssl_certificate /etc/apache2/cert/supersu-kr-cloudflare.pem;
    ssl_certificate_key /etc/apache2/cert/supersu-kr-cloudflare.key;

    # 2025.06.25 - 보안설정

    # TRACE 나 TRACK 으로 요청오는 경우 405 반환
    if ($request_method ~ ^(TRACE|TRACK)$) {
        return 405; # Method Not Allowed
    }

    # Range Header 가 비정상적으로 큰 경우 444 반환
    if ($http_range ~ "\d{9,}") {
        return 444; # No Resposne, Nginx Custom Error Code
    }

    proxy_pass_header Server; # Response Code 에 Server 값을 노출시키지 않음


    # location ~ \.php$ {
    #     proxy_pass http://127.0.0.1:8080;
    #     proxy_set_header Host $host;
    #     proxy_set_header X-Real-IP $remote_addr;
    #     proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    #     proxy_set_header X-Forwarded-Proto $scheme;
    # }

    access_log /var/log/nginx/access.log combined_vhost;
}

server {
    listen 443 ssl;
    listen [::]:443 ssl;
    http2 on;

    server_name blog.supersu.kr;
    root [Censored];

    ssl_certificate /etc/apache2/cert/supersu-kr-cloudflare.pem;
    ssl_certificate_key /etc/apache2/cert/supersu-kr-cloudflare.key;

    # 2025.06.25 - 보안설정

    # TRACE 나 TRACK 으로 요청오는 경우 405 반환
    if ($request_method ~ ^(TRACE|TRACK)$) {
        return 405; # Method Not Allowed
    }

    # Range Header 가 비정상적으로 큰 경우 444 반환
    if ($http_range ~ "\d{9,}") {
        return 444; # No Resposne, Nginx Custom Error Code
    }

    proxy_pass_header Server; # Response Code 에 Server 값을 노출시키지 않음

    index index.php index.html index.htm; # index 파일 우선순위

    set $no_cache 0; # 특정 조건에 대한 $no_cache 변수 정의 값

    if ($request_method = POST) { # POST 는 Cache 하지 않음
        set $no_cache 1;
    }

    if ($query_string != "") { # 쿼리 스트링이 없는 경우에는 PHP 로 가야되므로 Cache하지 않음
       set $no_cache 1;
    }

    # 관리자 사이트, 기타 wp 관련 내장 사이트는 Cache 하지 않음
    if ($request_uri ~* "(/wp-admin/|/xmlrpc.php|/wp-(app|cron|login|register|mail).php|wp-.*.php|/feed/|index.php|wp-comments-popup.php|wp-links-opml.php|wp-locations.php|sitemap(_index)?.xml|[a-z0-9_-]+-sitemap([0-9]+)?.xml)") {
        set $no_cache 1;
    }

    if ($http_cookie ~* "comment_author|wordpress_[a-f0-9]+|wp-postpass|wordpress_no_cache|wordpress_logged_in") { # 로그인 했거나, 최근에 댓글 단 사람은 Cache 하지 않음
        set $no_cache 1;
    }

    location / { # WordPress 용 Permalinks 처리 위한 로직
        try_files $uri $uri/ /index.php?$args;
        # 1. ($uri) 정적파일 먼저 검색
        # 2. ($uri/) / 로 끝난다면 해당 디렉토리의 기본파일 제공
        # 3. (/index.php?$args) 위 2개에 해당되지 않는 요청일 경우 /index.php 로 매개변수를 전달함
    }

    location ~* /(wp-config\.php|wp-settings\.php|readme\.html|license\.txt|readme\.txt)$ { # 보안을 위한 일부 파일은 404 오류 처리
        return 404;
    }

    location ~ \.php$ { # php 설정이 기본 요청보다 더 위에서 처리하도록 함
        include snippets/fastcgi-php.conf;
        fastcgi_pass unix:/run/php/php8.1-fpm.sock; # Nginx 가 .php 확장자를 가진 요청을 처리하기 위해 FastCGI 프로토콜을 사용
        fastcgi_split_path_info ^(.+\.php)(/.+)$; # 요청된 URI 에서 스크립트 이름과 추가적인 경로 정보를 분리하도록 함
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; # PHP_FPM 이 실행할 실제 PHP 파일의 전체 경로를 지정
        fastcgi_param PATH_INFO $fastcgi_path_info; # 파싱된 추가 경로 정보를 PHP 스크립트에 전달
        # fastcgi_index index.php; # 파일명이 지정되지 않은 PHP 요청인 경우 index.php 를 실행하도록 함 (2025.06.28 - Nginx + PHP 설정 시 include snippets/fastcgi-php.conf 로 중복 설정 되어있어 비활성화.)
        fastcgi_buffers 64 256k; # FastCGI Backend로부터 받은 응답을 Memory 에 저장하기 위한 버퍼의 개수와 각 버퍼의 크기를 설정
        fastcgi_buffer_size 256k; # FastCGI Backend로부터 받은 응답의 Header 를 저장하기 위한 Buffer 의 크기를 설정
        fastcgi_max_temp_file_size 1024m; # fastgti_buffers 로 할당한 Memory Buffer 를 초과했을 때 초과된 부분을 Disk 에 임시파일로 저장할 최대 크기 설정
        include fastcgi_params; # 표준 fastCGI 매개변수를 사용
        # fastcgi_intercept_errors off; # PHP 스크립트에서 4xx 5xx 에러가 발생하면 Nginx 가 이를 가로채서 자체적으로 정의된 에러 페이지로 리턴하고 싶은 경우

        fastcgi_cache WORDPRESS; # fastCGI Key 값 적용
        fastcgi_cache_valid 200 301 302 10m; # 200, 301, 302 응답은 10분 캐시
        fastcgi_cache_valid 404 1m; # 404 응답은 1분 캐시
        fastcgi_no_cache $no_cache; # $no_cache 변수에 따라 Cache 하지 않음
        fastcgi_cache_bypass $no_cache; # $no_cache 변수에 따라 캐시가 있어도 Backend 에 다시 요청을 보냄
    }

    add_header X-Cache-Status $upstream_cache_status;

    # Apache2 로 종속 처리하지 않고, Nginx + PHP 조합으로 진행함. 2025.06.28
    # location / {
    #     proxy_pass http://127.0.0.1:8080;
    #     proxy_set_header Host $host;
    #     proxy_set_header X-Real-IP $remote_addr;
    #     proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    #     proxy_set_header X-Forwarded-Proto $scheme;
    # }

    access_log /var/log/nginx/access.log combined_vhost;
}

 

기본적으로 IP 에 대한 접근을 방지하기 위해 default_server 값을 넣고 아래 server scope 에 해당 안되는 모든 요청은 444 Return 을 하도록 설정했다.

참고로 444 Return 은 Nginx 에서 있는 Custom HTTP Response Code 로, No Response 를 응답한다.


TRACE 나 TRACK 에 대한 HTTP Method 는 내 서버에서 사용하지 않으므로 모두 405 (Method Not Allowed) 를 리턴하도록 설정했다.


Range Header 조작을 통한 DoS 공격이 있을 수 있다고 판단해서 Range 가 비정상적이어도 444 Return 을 하도록 설정했다.


어짜피 이미 Nginx 라는걸 이용하고 있지만 어떤 Nginx 버전인지 알기 어렵게 하려고 proxy_pass_header Server; 값을 추가해서 헤더에 Server 값이 노출되지 않게 했다. 물론 기본적으로는 Cloudflare 가 보일 것 이다.


POST 요청, Query String 없음, 관리자 페이지를 포함한 wp 관련 특정 페이지, 특정 Cookie 가 존재하는 경우(로그인 / 최근 댓글 생성 등)일 경우에는 Cache 하면 안되는 페이지 형태이므로 no-cache 값이 true 로 지정이 되어있다.


fastcgi_cache값이 핵심적인 서버 속도 증가의 포인트로 상단에 Nginx.conf 문단에서 설정한 RAM Disk 를 활용해서 해당 폴더에 파일을 생성하고 빠르게 캐싱된 사이트를 표시해주는 것으로 속도를 상승 시켰다.

 

서버 접속 결과

첫 접속 페이지 결과
첫 접속 페이지 결과

 

Waiting for server bresponse 값이 1초 정도 사이트 로드가 걸렸던 홈페이지가 150ms 정도로 매우 낮아져서 최종 200ms 정도내로 사이트가 불러와졌다.

 

물론 로그인한 사용자는 Cache 된 데이터를 쓰지 않기 때문에 속도가 이보다는 떨어지게 처리되지만 어짜피 관리하는 사람 입장에서는 나 혼자밖에 없으므로 내가 속도 떨어지는것은 염두하지 않아도 된다고 판단했다.