워드프레스 개발할 때 사용하려고 mytory-basic 이라는 테마를 만들어 두고 사용하려고 한다. 이 테마는 기본적으로 OOCSS로 개발하려고 하고 있으며, BEM 작명법을 따른다. inuit.css(ver 4.1.5)를 사용한다.

그런데 워드프레스의 ‘메뉴’ 기능을 사용하니까 문제가 생기는 게, 1단계 li와 2단계 li를 구분하는 클래스가 없다. 그래서 계층형 클래스를 사용하게 된다. sub-menu를 가진 li와 가지지 않은 li도 구분이 안 된다.

그래서 일단, 1단계 limain-nav__li1이라는 클래스를 붙였다. 2단계 li에까지 붙이는 건 시간 관계상 안 했다. 또한 sub-menu를 가진 li에는 main-nav__li1--has-child라는 클래스를 붙이도록 만들었다.

이 과정을 공유한다. 이제부터 시작.

작업을 하고 나면 아래처럼 메뉴를 호출하게 된다. (파일은 header.php)

$opt = array(
    // ...
    'walker' => new MBT_Walker_Nav_Menu,
);
wp_nav_menu($opt);

walker라는 옵션에 주목하자. 위 옵션은 쭈욱 돌면서 메뉴를 뿌려 주는 역할을 하는 객체를 넘겨 주는 옵션이다. 웬만해선 건드릴 일이 없다.

전에 한 번 이걸 고치는 예제를 본 적이 있는데 ‘이게 도대체 뭐야?’ 하는 생각만 하고 볼 엄두를 못 낸 적이 있다. 그런데 오늘 옵션 쪽을 뜯어 보다 보니, 내가 자세히 보지 않아서 이해를 못 했던 것이라는 걸 깨달았다. 객체를 우리가 직접 짜야 하는 게 아니었다. 복사해서 붙여넣고 조금만 수정해 주면 되는 거였다.

기본 walker를 뜯어 보자

wp-includes/nav-menu-template.php 파일에 있는 wp_nav_menu 함수를 살펴 보자. walker 옵션의 기본값은 그냥 빈 문자열이다. 뭐지?

219번째 줄에 가 보면 비밀을 알 수 있다(이하 모든 줄번호는 WP 3.6 기준이다).

$items .= walk_nav_menu_tree( $sorted_menu_items, $args->depth, $args );

walk_nav_menu_tree라는 함수에 세 값을 넘겨 주는데, $args가 우리가 세팅한 옵션값을 갖고 있는 놈이다. 우린 배열로 세팅해서 넘겼지만, 중간에 객체 형식으로 바뀌어서, 우리가 넘겨 준 walker 값은 $args->walker에 세팅돼 있다.

walk_nav_menu_tree 함수를 살펴 보자.

function walk_nav_menu_tree( $items, $depth, $r ) {
    $walker = ( empty($r->walker) ) ? new Walker_Nav_Menu : $r->walker;
    $args = array( $items, $depth, $r );

    return call_user_func_array( array($walker, 'walk'), $args );
}

간단하게 돼 있다. 세 번째 인자값인 $r이 바로 $args다. 위 코드의 두 번째 줄을 보면 $walker를 세팅하는 게 나온다. 비어 있으면 Walker_Nav_Menu 객체를 생성해서 받게 돼 있다. 그렇게 $walker라는 변수에 객체를 세팅한 뒤, 3번째 줄에서 $args를 세팅해서 call_user_func_array 함수로 $walker->walk($args)를 호출한다(5번째 줄).

즉, 메뉴를 뿌리는 함수들은 Walker_Nav_Menu 객체에 들어 있으며, 이걸 교체해 주면 된다는 말이다. 이제 Walker_Nav_Menu 클래스를 뜯어 보자.

Walker_Nav_MenuWalker 클래스

Walker_Nav_Menu 클래스는 wp-includes/nav-menu-template.php 파일의 17번째 줄부터 정의돼 있다. 아래처럼 시작한다.

class Walker_Nav_Menu extends Walker {

여기서 알 수 있는 것은, Walker_Nav_MenuWalker를 상속한다는 것이다. 아까 walk 함수를 호출해서 메뉴를 그린다고 했는데, Walker_Nav_Menu에는 walk 함수가 없다. 그럼 부모 클래스인 Walker 클래스에 있을 것이다. 그리로 찾아 가 보자.

Walker 클래스는 wp-includes/class-wp-walker.php 파일의 15번째 줄에 정의돼 있다. walk 함수도 갖고 있다. 174번째 줄이다.

walk 함수를 뜯어 보다가 230번째 줄에서 찾던 걸 찾았다. 아래 코드다.

foreach ( $top_level_elements as $e )
    $this->display_element( $e, $children_elements, $max_depth, 0, $args, $output );

탑 레벨 요소면 display_element 함수에 $children_elements를 넣어서 뿌리게 되는 거다. 와우!

$e가 바로 탑 레벨 요소의 정보고, $e->classes 배열에 클래스 이름이 들어간다. 좋아! 여기를 고칠까? 아니다. 여긴 워드프레스 코어 파일이다. 코어 파일을 직접 수정하면 워드프레스를 버전업할 때 내가 수정한 게 사라진다. 테마에서 해결해야 한다.

지금 고쳐야 하는 건 walk 함수니까, Walker_Nav_Menu 클래스를 상속받는 새로운 클래스를 만들어서 wp_nav_menu 함수의 walk 옵션에 넘겨 주면 되겠다. 클래스를 만들어 보자.

// Walker_Nav_Menu 클래스를 상속. MBT는 Mytory Basic Theme.
class MBT_Walker_Nav_Menu extends Walker_Nav_Menu{

    // 주석은 생략
    function walk( $elements, $max_depth) {
        // walk 함수를 그대로 복사했다.
    }
}

이렇게 간단한 구조다. 나는 functions-nav.php라고 파일을 만들어서 위 코드를 넣었고, functions.php에서 인클루드했다.

다음으로 할 일은, MBT_Walker_Nav_Menu 클래스의 walk 함수 일부를 수정하는 것이다. foreach ( $top_level_elements as $e ) 부분을 찾아가서 두 줄을 아래처럼 수정했다. 도움을 주기 위해 주석을 달았다. 실제 코드엔 이런 주석 없다.

foreach ( $top_level_elements as $e ){

    // 탑 레벨이니까 1번째 li임을 나타내기 위해 li1이라고 클래스 붙임.
    $e->classes[] = 'main-nav__li1';

    // $children_elements가 있으면 추가로 has-child라고 클래스 붙임.
    if( ! empty($children_elements[$e->ID])){
        $e->classes[] = 'main-nav__li1--has-child';
    }

    // 아래는 원 코드와 같음.
    $this->display_element( $e, $children_elements, $max_depth, 0, $args, $output );
}

오케이. 이렇게 하고 나서 아래처럼 메뉴를 호출해 주면 된다.

$opt = array(
    'theme_location' => 'main-nav', 
    'walker' => new MBT_Walker_Nav_Menu,
);
wp_nav_menu($opt); 

그러면 모든 탑 레벨 li에는 main-nav__li1이라는 클래스가 붙고, sub-menu를 가진 li에는 main-nav__li1--has-child라는 클래스가 추가로 붙는다.

나머지 응용은 각자 알아서. depth에 따라 li에 숫자를 붙이는 것도 만들고 싶지만, 시간 관계상 끝이다.