알짜게시판

캐시 환경에서 AJAX 댓글(코멘트) 분리로 서버 응답시간 속도 향상

기존 로그인 사용자 페이지 처리 방식

워드프레스 기본 구조에서는 로그인 사용자가 페이지를 요청하면 캐시가 적용되지 않는다. 요청이 들어오면 서버는 로그인 상태를 확인하고, 사용자 정보를 포함한 HTML을 매번 새로 생성한다. 이 과정에서 댓글 폼 생성, 사용자 정보 조회, 댓글 리스트 렌더링까지 함께 처리된다.

입력: 페이지 요청
조건: 로그인 사용자
처리: PHP에서 HTML 전체 생성
결과: 동적 페이지 응답

처음에는 이 방식이 당연하다고 생각했다. 사용자 상태가 다르기 때문에 HTML을 공유하면 안 된다고 판단했기 때문이다. 하지만 실제 운영에서는 로그인 상태에서 서버 응답 시간이 2초 이상으로 늘어나는 구간이 반복적으로 발생했다. 캐시를 사용하지 않는 요청이 누적되면서 서버 처리 시간이 길어지는 구조였다.

댓글 기능이 서버 응답시간에 영향을 주는 구조

댓글 기능이 포함된 페이지에서는 서버가 처리해야 할 작업이 더 많아진다. 댓글 폼은 로그인 상태에 따라 다르게 생성되고, 댓글 리스트도 함께 렌더링된다. 이 과정이 페이지 생성 단계에 포함되면서 응답 시간이 증가한다.

입력: 댓글 활성화 페이지 요청
조건: 사용자 상태 포함
처리: 댓글 관련 HTML 생성
결과: 서버 처리 시간 증가

처음에는 댓글 영역만 별도로 처리하려고 했다. 일부 영역만 비캐시 처리하는 방식도 시도했지만, 서버 설정과 CDN 환경에 따라 결과가 달라졌다. 이 방식으로 통일하려 했지만 일부 상황에서는 다시 나눠서 처리해야 했고, 결국 댓글 기능 자체를 분리하는 방향으로 변경했다.

AJAX 댓글 분리로 처리 흐름 변경

댓글 기능을 AJAX로 분리하면서 페이지 HTML과 사용자 상태를 분리했다. 페이지는 항상 동일한 HTML을 반환하고, 사용자 정보와 댓글 처리만 별도의 요청으로 수행한다.

<?php
/*
Plugin Name: Kilho's Ajax Comment
Plugin URI: http://www.kilho.net/
Description: 길호넷에 사용되는 Ajax 댓글 플러그인입니다.
Author: Kilho, Oh
Version: 0.9
Author URI: http://www.kilho.net/
*/

defined( 'ABSPATH' ) || exit;

define( 'KH_COMMENT_VER', '0.9' );
define( 'KH_COMMENT_URL', plugin_dir_url( __FILE__ ) );

class KH_Ajax_Comment {

    private $saved_user = null;

    public function __construct() {
        add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_assets' ) );
        add_action( 'wp_ajax_kh_submit_comment', array( $this, 'handle_submit' ) );
        add_action( 'wp_ajax_nopriv_kh_submit_comment', array( $this, 'handle_submit' ) );
        add_action( 'wp_ajax_kh_comment_session', array( $this, 'ajax_session' ) );
        add_action( 'wp_ajax_nopriv_kh_comment_session', array( $this, 'ajax_session' ) );
        add_action( 'comment_form_before', array( $this, 'spoof_anonymous' ) );
        add_action( 'comment_form_after', array( $this, 'restore_user' ) );
        add_filter( 'pre_comment_approved', array( $this, 'block_user_email_impersonation' ), 10, 2 );
    }

    public function block_user_email_impersonation( $approved, $commentdata ) {
        if ( ! empty( $commentdata['user_id'] ) ) {
            return $approved;
        }
        if ( empty( $commentdata['comment_author_email'] ) ) {
            return $approved;
        }
        if ( get_user_by( 'email', $commentdata['comment_author_email'] ) ) {
            return 0;
        }
        return $approved;
    }

    public function spoof_anonymous() {
        if ( ! is_user_logged_in() ) {
            return;
        }
        global $current_user;
        $this->saved_user = $current_user;
        $current_user     = new WP_User( 0 );
    }

    public function restore_user() {
        if ( null === $this->saved_user ) {
            return;
        }
        global $current_user;
        $current_user     = $this->saved_user;
        $this->saved_user = null;
    }

    public function ajax_session() {
        nocache_headers();

        $logged_in = is_user_logged_in();
        $response  = array(
            'logged_in' => $logged_in,
            'nonce'     => wp_create_nonce( 'kh_comment_nonce' ),
            'name'      => '',
            'email'     => '',
            'url'       => '',
        );

        if ( $logged_in ) {
            $user              = wp_get_current_user();
            $response['name']  = $user->display_name ? $user->display_name : $user->user_login;
            $response['email'] = $user->user_email;
            $response['url']   = $user->user_url;
            $response['logged_in_as_html'] = sprintf(
                '<p class="logged-in-as">%s(으)로 로그인 됨. (* 질문, 건의사항 등은 "질문게시판"을 이용해주세요)</p>',
                esc_html( $response['name'] )
            );
        } else {
            $commenter         = wp_get_current_commenter();
            $response['name']  = $commenter['comment_author'];
            $response['email'] = $commenter['comment_author_email'];
            $response['url']   = $commenter['comment_author_url'];
        }

        wp_send_json_success( $response );
    }

    public function enqueue_assets() {
        if ( ! is_singular() || ! comments_open() ) {
            return;
        }

        wp_enqueue_script(
            'kh-comment',
            KH_COMMENT_URL . 'assets/kh-comment.js',
            array( 'jquery' ),
            KH_COMMENT_VER,
            true
        );

        wp_localize_script(
            'kh-comment',
            'KHComment',
            array(
                'ajaxurl' => admin_url( 'admin-ajax.php' ),
                'nonce'   => wp_create_nonce( 'kh_comment_nonce' ),
                'i18n'    => array(
                    'submitting' => __( '전송 중…', 'kh-comment' ),
                    'error'      => __( '댓글 전송에 실패했습니다. 잠시 후 다시 시도해 주세요.', 'kh-comment' ),
                ),
            )
        );
    }

    public function handle_submit() {
        if ( ! isset( $_POST['kh_comment_nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['kh_comment_nonce'] ) ), 'kh_comment_nonce' ) ) {
            wp_send_json_error(
                array( 'message' => __( '보안 검증에 실패했습니다. 페이지를 새로고침 후 다시 시도해 주세요.', 'kh-comment' ) ),
                403
            );
        }

        $comment = wp_handle_comment_submission( wp_unslash( $_POST ) );

        if ( is_wp_error( $comment ) ) {
            $error_data = (int) $comment->get_error_data();
            $status     = $error_data ? $error_data : 400;
            wp_send_json_error(
                array(
                    'message' => $comment->get_error_message(),
                ),
                $status
            );
        }

        $user           = wp_get_current_user();
        $cookies_consent = isset( $_POST['wp-comment-cookies-consent'] );
        do_action( 'set_comment_cookies', $comment, $user, $cookies_consent );

        $comment_html = $this->render_comment_html( $comment );

        $response = array(
            'comment_id'   => $comment->comment_ID,
            'parent_id'    => (int) $comment->comment_parent,
            'approved'     => (int) $comment->comment_approved,
            'html'         => $comment_html,
            'message'      => 1 === (int) $comment->comment_approved
                ? __( '댓글이 등록되었습니다.', 'kh-comment' )
                : __( '댓글이 등록되었으며, 승인 후 표시됩니다.', 'kh-comment' ),
        );

        wp_send_json_success( $response );
    }

    private function render_comment_html( $comment ) {
        $args = array(
            'style'       => 'ul',
            'short_ping'  => true,
            'avatar_size' => 80,
            'format'      => current_theme_supports( 'html5', 'comment-list' ) ? 'html5' : 'xhtml',
            'max_depth'   => (int) get_option( 'thread_comments_depth' ),
        );

        if ( function_exists( 'porto_comment' ) ) {
            $args['callback'] = 'porto_comment';
        }

        $walker = new Walker_Comment();
        $depth  = 1;

        if ( $comment->comment_parent ) {
            $depth = $this->get_comment_depth( $comment->comment_parent ) + 1;
        }

        $output = '';
        $walker->start_el( $output, $comment, $depth, $args );
        $walker->end_el( $output, $comment, $depth, $args );
        return $output;
    }

    private function get_comment_depth( $comment_id ) {
        $depth   = 1;
        $current = get_comment( $comment_id );

        while ( $current && $current->comment_parent ) {
            $depth++;
            $current = get_comment( $current->comment_parent );
        }

        return $depth;
    }
}

new KH_Ajax_Comment();

입력: 페이지 요청
조건: 사용자 상태 제거
처리: 캐시된 HTML 반환
결과: 동일한 응답 구조

이 구조에서는 HTML 생성 과정에서 사용자 상태를 제거하기 때문에 캐시를 유지할 수 있다.

서버 응답시간 기준으로 본 처리 구조 변화

구조 변경 이후 가장 큰 변화는 서버 응답 단계에서 발생한다. 기존에는 요청마다 HTML을 생성해야 했지만, 변경 이후에는 캐시된 HTML을 그대로 반환한다.

기존 방식
입력: 요청
처리: 사용자 판단 → HTML 생성
결과: 응답 지연

변경 방식
입력: 요청
처리: 캐시 반환
결과: 빠른 응답

실제 측정 기준으로 로그인 상태에서 서버 응답 시간이 기존 1초 이상에서 수십 ms 수준으로 줄어들었다. 요청 처리 자체가 단순해지면서 서버가 수행하는 작업이 크게 줄어든 결과다.

댓글 작성 요청과 서버 처리 흐름

댓글 작성 요청은 기존과 동일하게 1회 요청으로 처리된다. 다만 처리 범위가 다르다. 기존에는 댓글 저장 이후 페이지 전체를 다시 생성했지만, 변경 이후에는 댓글 저장만 수행한다.

입력: 댓글 작성
조건: AJAX 요청
처리: 댓글 저장
결과: JSON 응답

처음에는 AJAX 요청이 추가되면서 서버 부하가 증가할 것이라고 생각했다. 하지만 실제로는 페이지 전체 렌더링이 제거되면서 오히려 처리량이 줄어드는 구조였다.

적용 환경과 선택 기준

이 방식은 서버 응답시간을 줄이는 것이 중요한 환경에서 적합하다. 특히 로그인 사용자 비율이 높은 사이트에서는 캐시 적용 여부가 전체 성능에 큰 영향을 준다. 페이지 HTML을 정적으로 유지하고, 동적인 요소만 분리하는 구조이기 때문이다.

입력: 페이지 요청
조건: 캐시 적용 가능
처리: 빠른 응답 반환
결과: 서버 부하 감소

이 구조는 HTML과 데이터 처리를 분리하는 것을 전제로 한다. 댓글 기능이나 사용자 상태를 서버 렌더링에 포함시키지 않는 경우에 효과적으로 동작한다.

로그인 후 댓글내용을 입력해주세요

제목 글쓴이 조회 날짜
워드프레스 캐시 환경에서 AJAX 댓글(코멘트) 분리로 서버 응답시간 속도 향상 13 51분 전
리눅스 아파치 일반 계정 파일 접근 권한 설정과 mod-ruid2 적용 방법 136 26-04-26
델파이 Delphi IDE 메인 폼 안열림 dproj 설정으로 해결 110 26-04-26
개발팁 VSCode 한국어 설정법, Configure Display Language로 UI 언… 151 26-04-24
기타 RustDesk 자체 서버 구축으로 공용 서버 로그인 문제 해결 268 26-04-16
리눅스 Dante SOCKS5 프록시 서버 구축 및 설정 312 26-04-10
윈도우 윈도우 실행파일(exe) 서비스 등록 자동화 쉽게 하는 방법 - NSSM 427 26-04-05
기타 CLIProxyAPI Windows 설치 가이드 709 26-04-04
워드프레스 그누보드7 데이터를 WordPress WXR로 변환하기 428 26-04-01
기타 Ollama 설치 경로 C드라이브 고정 문제 해결 및 드라이브 변경 방법 422 26-03-31
개발팁 Apache Access Log 기반 도메인별 방문자 카운터 구현 343 26-03-31
윈도우 ZImage AI 이미지 생성 환경 구축 방법 516 26-03-26
윈도우 파일 복사 후 실행 배치파일 - 에러 감지 포함 538 26-03-10
개발팁 네이버 검색 Open API 846 26-02-10
개발팁 네이버 검색광고 키워드 도구 API 검색 1,027 26-02-10
개발팁 MariaDB column_stats 테이블 오류 해결 방법 1,027 25-12-06
개발팁 PHP-FPM 에러 로그 실시간 모니터링 922 25-12-03
윈도우 윈도우11 업데이트 후 네트워크 드라이브 접근 불가 문제 해결 방법 1,678 25-11-24
개발팁 다국어 번역 함수 구현 방법 3,346 25-11-08
워드프레스 "치명적인 오류가 발생했습니다" 디버깅 가이드 14,142 25-10-31
리눅스 Apache 웹 로그 분석하기 – awk와 GoAccess 활용 18,728 25-10-28
길호넷 칼무리 - 외부 명령으로 캡처 자동화하기 1 40,450 25-10-14
윈도우 윈도11 강제업데이트 피하고 윈도10 계속 쓰기 41,886 25-10-12
리눅스 PHP 파일 업로드 용량 늘리기 56,798 25-10-03
개발팁 애드센스 충돌 문제 해결하기 74,782 25-09-20
파이썬 Python Playwright로 Edge 브라우저 제어하기 76,860 25-09-12
델파이 TDirectory.Delete 대신 CMD 으로 폴더 삭제 75,163 25-09-10
윈도우 Chrome 및 Edge를 TLS 1.2 모드로 실행 74,572 25-08-30
개발팁 애드센스 자동 광고 사용 시 빈 화면이 출력된다면? 159,431 25-06-20
윈도우 브라우저 환경설정 추출 146,043 25-06-18
워드프레스 xmlrpc.php 차단으로 보안 강화하기 137,152 25-06-05
회생의길파트너시스템책과지식주점닷컴웹툰인사이트