워드프레스 WP_Post 객체 content 전체 쿼리 막는법

결론 코드부터

아는 사람은 바로 functions.php 넣으면 됨, 근데 고급 사용자용 이니까 어떻게 써야할 지 견적 안 나오면 그냥 안 쓰는걸 추천.

// replace query . posts.* to ID . hardfilter.com
function replace_posts_query( $query ) {
    if ( !is_feed() && ( is_home() || is_archive() || ( is_search() && !is_admin() ) ) ) {
        global $wpdb;
        $query = str_replace( "$wpdb->posts.*", "$wpdb->posts.ID", $query );
    }
    return $query;
}
add_filter( 'query', 'replace_posts_query' );
add_filter('pre_wp_nav_menu', function() { remove_filter('query', 'replace_posts_query'); });
  1. is_* 조건문으로 본인이 원하는 범위를 지정
  2. $wpdb->posts.* 를 replace 할 범위를 2번째 인자에 지정
  3. 범위 추가하려면 "$wpdb->posts.ID, $wpdb->posts.post_title" 이런식으로
  4. 예제에서는 $wpdb->posts.ID 만 사용해서, WP_Post 객체 생성시 포스트 ID 만 효율적으로 생성
  • 저대로 쓰면 오직 ID 값만 존재하기 때문에 ID 값만 활용해 직접 SQL or 캐싱 시스템을 만들어야함, 고로 압도적 성능 최적화 가능
  • ↑ 할 줄 모르면 범위를 애초에 ID 만 하는게 아니라 필요한 만큼 확장해서 쓰면 됨(e.g. 타이틀, 링크, etc.)
  • singular 에선 쿼리가 이루어지고, archive 등에서만 쿼리를 막아야 하기 때문에 필수적으로 영역을 지정해야 함.
  • is_search 에서 !is_admin 안 걸면 대시보드에서 포스트 검색 안됨
  • !is_feed 걸어야 피드 제대로 표현됨
  • 네비게이션 메뉴에서도 쿼리를 변형하면 제대로 출력이 안 되기 때문에 pre_wp_nav_menu 필터로 네비쪽에선 비활성화 함.

보너스 코드

posts 를 교체해 전체 콘텐츠 쿼리를 막는 것 말고도 더 최적화 할 수 있다.

FROM wp_postmeta 를 찾아 메타정보 쿼리를 막을 수 있다(e.g. id, mete_key, etc.)

function remove_meta_query($query) {
    if (strpos($query, 'FROM wp_postmeta') !== false) {
        return '/**/';
    }
    return $query;
}
add_filter('query', 'remove_meta_query');

t.term_id, tr.object_id 를 찾아 taxonomy 쿼리를 막을 수 있다(e.g. 카테고리, 태그, etc.)

function remove_term_query($query) {
    if (strpos($query, 't.term_id, tr.object_id') !== false) {
        return '/**/';
    }
    return $query;
}
add_filter('query', 'remove_term_query');

return을 빈 값이나 null 하면 오류나기 때문에 안전하게 주석을 반환해 발동은 하지만 쿼리할게 없게 만든다.

이 두 보너스코드와 posts코드를 합치려면 이렇게 쓰면 된다.

// replace query full . hardfilter.com
function query_set_c($query) {
    if ( !is_feed() && ( is_home() || is_archive() || ( is_search() && !is_admin() ) ) ) {
		global $wpdb;
        $query = str_replace("$wpdb->posts.*", "$wpdb->posts.ID", $query);
        if (strpos($query, 'FROM wp_postmeta') !== false || strpos($query, 't.term_id, tr.object_id') !== false) {
            return '/**/';
        }
    }
    return $query;
}
add_filter('query', 'query_set_c');
add_filter('pre_wp_nav_menu', function() { remove_filter('query', 'query_set_c'); });

WP_Post 의 문제점

워드프레스는 굉장히 비효율적으로 포스트 관련 내장 함수를 하나라도 쓰는 순간 쿼리시 WP_Post 객체가 자동 생성되고 메모리에 로드된다.

이후에는 메모리에 로드(페이지 끝나면 산화됨)된 내용을 참조하기 때문에 효율적인 논리라고 생각하는거 같은데 터무늬없다.

왜냐면 WP_Post 객체 생성시 $wpdb->posts.* 를 하기 때문에, 해당 포스트의 콘텐츠 전문을 쿼리하기 때문.

쉽게말해 포스트를 길게 쓰는 사람이 아니라도 평균적으로 굉장히 짧게 잡아 2000자 정도를 쓴다고 치자(1000자로 예를 들려고 했는데 지금 이 포스트의 이 지점까지만 와도 2000자가 넘는다).

아카이브에 한 페이지당 글이 20개 필요하면, 워드프레스 내장 함수를 단 하나라도 쓰는 순간 무조건 20000자의 본문도 반드시 쿼리된다.

게다가 본문만으로 끝나는건 당연히 아니고 +α 도 잔뜩 추가된다(e.g. meta, taxo, etc.).

백 번 양보해서 +α 는 그렇다 치더라도, 전체 콘텐츠를 로드하는 순간 무조건 손해가 발생할 수 밖에 없는 구조다.

아무리 최대한 긍정적으로 여러 변수를 종합 고려해도 대부분의 유저, 대부분의 상황에서 무조건 손해다.

근데 기본 기능으로 안전하게 저걸 막을 수 있는 설정 방법이 없다(나는 강제로 해낸거니까 예외).

아무리 테마를 하나부터 열까지 뜯어고쳐도, 워프 내장기능부터 오는거기 때문에 WP_Post 객체 생성을 완벽히 제거하는건 불가능.

또한 이걸 효율적으로 캐시하는것도 불가능하다.

결과값을 아무리 효율적으로 캐시해도 내장 함수가 발동되는 순간 무조건 객체가 또 생성되고, 그 이후에 캐시가 작동하기 때문에 캐시를 하면 오히려 비용이 증가하는 굉장히 충격적인 구조다.

때문에 차선책으로 WP_Post 객체가 생성되더라도, 애초에 쿼리파트를 str_replace 변조해서 전체 쿼리를 막는것, 현재로선 유일한 해법.

이렇게 ID 만 쿼리하게 하고, 해당 ID 를 통해 효율적인 쿼리 or 캐시구조를 만들면 됨, 아니면 적절히 필요하게 조절하던가.

워드프레스 내장 비효율성에 대해 할 말이 정말 많은데 여러가지를 고려해 이만 줄임.