React, Go로 만든 WordPress 읽기 서비스 구축 사례: 1편 개요

Popit 서비스의 UI가 확 달라졌습니다. 기존의 워드프레스 틀에서 벗어나 아주 자유로운 구성으로 서비스를 개편하였습니다. 물론 전문 디자이너의 지원을 받지 못해 아쉬운 부분이 있지만, 앞으로는 쉽게 화면을 변경할 수 있는 기반을 구축하였다는데 의미가 있다고 할 수 있습니다. 앞으로 몇회에 걸쳐 Popit 서비스 개편하면서 작업한 내용에 대해 공유할 예정입니다. 이번 글에서는 왜 개편 작업을 하게 되었는지와 시스템의 구성 및WordPress의 데이터 모델에 대해 간단하게 살펴보겠습니다.

왜 바꾸었나?

2016.6월 경에 개발자 3명이 모여 popit 서비스를 처음 기획하던 당시에는 개발자들끼리 모였지만 "개발은 하지 말고 컨텐츠에 집중하자" 라고 하였습니다. 그래서 서비스 솔루션으로 워드프레스를 선택하고, 아무런 개발없이 다양한 플러그인과 테마를 활용하여 서비스를 구성하였습니다.  그 이후로 2년 가까이 서비스를 운영하면서 특별하게 불만은 없었지만 컨텐츠가 점점 쌓이고, 사용자가 늘어남에 따라 대략 다음과 같은 요구사항이 늘어 났습니다.

  • Popit의 컨텐츠는 최신글 뿐만 아니라 과거 글도 처음 서비스를 방문하는 개발자에게는 유익한 글이 될 수 있는데 최신글 위주로만 배치가 되어 있다.
  • 많은 개발자들이 글 집필에 참여하고 있는데 저자들이 부각되는 지면이 필요한 것 같다.
  • Session 당 Page view가 너무 낮은데 글을 읽을 때 유사글이나, 인기글 등을 독자들에게 같이 보여주는 것은 어떨까?
  • 아직 사용자가 많이 부족하지만 그래도 좀 더 많은 수익을 얻기 위해서는 여러 광고 등을 올릴 수 있는 준비가 되어 있어야 한다.

실제 개선 작업은 조금씩 진행중이었는데 dable 이라고 하는 국내 스타트업에서 추천기사 기반의 광고를 올려보자는 제안이 들어와서 빠르게 작업을 진행했습니다. 워드프레스 만으로도 쉽게 적용 가능하지만 진행하고 있었던 작업을 마무리 하기 위한 좋은 동기를 부여해 주었습니다.

워드프레스는 막강한 Plugin을 제공하고 있으며 심지어는 관리 웹에서 php 파일을 수정하여 다양한 형태로 커스터마이징 할 수 있는 훌륭한 솔루션입니다. 또한 WordPress 자체에도 풍부한 API를 제공하고 있어 별도의 API 서버를 구축할 필요도 없습니다. 하지만 이들 Plugin을 찾고 검증하는데에도 시간이 많이 소요되고, 좀 더 자유도를 높이기 위해 화면 계층을 별도로 개발하는 방식을 선택하였습니다[1].

제약 조건

가장 중요한 제약 조건은 URL에 대한 부분입니다. 현재 Popit의 대부분의 트래픽은 구글 검색을 통해 유입되고 있습니다. 따라서 공개되어 있는 글의 URL은 그대로 유지를 해야 합니다. 각 글의 URL 뿐만 아니라 저자 페이지, 카테고리/Tag URL 도 그대로 유지를 시켜야 합니다.

그 다음은 유지보수에 대한 이슈입니다. 자체 개발했을 때의 가장 문제가 유지보수가 어려워져 결국은 기존 솔루션을 그대로 사용하는 것보다 더 비효율적으로 바뀌는 경우를 많이 봤습니다. 특히 화면 관련 코드는 복잡한 자바스크립트, CSS, HTML 때문에 직접 만든 개발자가 아니면 수정하기 어려운 경우가 많이 있었습니다. 이런 상황을 방지하기 위해 화면 관련 코드는 HTML, CSS, JQuery 등을 많이 사용하지 않는 방식으로 개발되기를 원했습니다.

위 두가지를 별도로 보면 특별한 이슈는 없을 것 같았는데 이 두가지 제약조건이 합쳐진 결과는 아주 골치아픈 문제를 많이 발생시켰습니다. 간단한 예로, 두번째 조건을 위해 웹 관련 코드는 React를 기반으로 만들었는데 첫번째 조건인 URL을 그대로 유지해야 하는 조건이 들어오면서 React의 Hash Router 를 사용할 수 없었습니다. 그리고 검색엔진에서 특정 URL로 직접 접근이 가능하기 때문에 Server Side Rendering 까지 고려를 해야 하는 상황이 발생하게 되었습니다.

이런 제약 조건으로 인해 처음에는 쉽게 덤벼 들었다가 대책이 없는 상황까지 갔었는데 다행히 dable 적용이라는 미션으로 조금 더 힘내서 마무리를 할 수 있게 되었습니다.

시스템 기본 구성

글 작성, 사용자 관리, 카테고리 관리 등 CMS(Content Management System)이 가져야 하는 기본 기능 모두를 다시 개발하기에는 물리적인 시간도 부족하고, 워드프레스가 대부분 지원하는 기능을 굳이 다시 개발할 필요는 없었습니다. 따라서 글의 관리 기능은 워드프레스의 기능을 그대로 유지하면서 일반 사용자에게 나타나는 "글 조회" 기능만 개발을 하였습니다.

전체 시스템은 다음 그림과 같습니다.

popit_v2_components

각 솔루션 또는 계층 별로는 다음과 같은 역할을 수행합니다.

  • WordPress:
    • 글 작성, 사용자 관리, 카테고리 관리 등 기존 Wordpress의 관리자 기능은 기존 WordPress 프로그램의 기능을 그대로 사용한다.
    • 데이터베이스도 기존의WordPress 데이터베이스를 그대로 사용한다.
  • Golang 기반 API 서버:
    • WordPress에서 관리하는 글 관련 데이터베이스를 조회하여 화면을 만드는데 필요한 데이터를 json 형태로 제공한다.
    • 이 서버가 사용하는 데이터베이스 역시 WordPress 데이터베이스를 사용한다.
    • 추가로 Google AdSense 설정 및 기타 정보 설정을 위해 별도의 테이블을 몇개 생성하였다.
  • ReactJS 기반 WebUI:
    • 웹 또는 모바일웹 화면을 구성하는 역할을 수행하며 대부분의 데이터는 API 서버를 통해 받는다.
  • React Server Side Render Engine:
    •  검색을 통해 특정 URL로 직접 접근하거나, 검색 엔진 또는 페이스 북 등의 크롤러 요청에 대응하기 위해 브라우저에서 화면을 생성하지 않고, 웹 서버 측에서 화면을 생성하여 전달하는 기능을 수행한다.
    • 하나의 React 코드로 서버 사이드 렌더링과 브라우저 렌더링을 동시에 지원하도록 구성하였으며 서버 사이드에서 ReactJS를 실행하기 위해 node express  서버를 사용한다.
  • 검색 서버:
    • WordPress에서 제공하는 검색 기능 이외에 자체 개발한 Lucene 기반 검색을 위한 서버이다. 현재 검색 랭킹에 대한 부분은 거의 고려되어 있지 않으며, 검색 시 각 필드(제목, 본문, 카테고리 등)에 대한 가중치를 조절하는 수준으로 랭킹을 조절하고 있다.

공개된 소스 코드

위 모든 소스 코드는 공개 되어 프로젝트 별로 레포지토리는 다음과 같습니다.

위 프로젝트는 모두 GPLv2 라이센스를 따르고 있습니다. 좀 더 자유로운 라이센스를 하고 싶었지만 WordPress 자체가 Plugin 등에 대해서는 GPLv2  (또는 이상)의 라이센스를 권장하고 있기 때문에 이를 따랐습니다[2].

버그 등이 있으면 이슈로 등록해주시면 처리하도록 하겠습니다. 직접 수정된 코드를 보내주셔도 됩니다.

WordPress 테이블 구조 및 Query

WordPress 읽기 전용 페이지를 구성하기 위해서는 데이터 모델을 파악하는게 우선입니다. 모든 테이블의 내용은 파악하지 않았고 Popit 서비스에 필요한 테이블만 조사하였습니다. WordPress 공식 사이트에도 테이블에 대한 내용 및 ERD를 확인할 수 있습니다.

WordPress 데이터베이스는 특별한 옵션을 주지 않으면 모든 테이블 명에 "wp_" 와 같은 prefix 가 붙어 있습니다. Popit 서비스는 2년전에 설치하였기 때문에 WordPress 4.5.14 버전을 사용하고 있습니다.

전체 테이블 중에서 Popit 서비스를 위해서는 글과 관련된 정보인 포스트, 사용자, 카테고리, 태그 관련 정보만 필요합니다.  다른 부분은 크게 복잡하지 않은데 특정 글(Post)의 카테고리나 태그 정보를 가져올 때는 연관 관계에 있는 테이블을 몇번 거쳐야만 가능합니다.

Popit 서비스를 위해 사용되는 데이터에 대한 SQL은 대략 다음과 같습니다.

  • 모든 게시글 가져오기
    1
    2
    3
    4
    select *
    from wp_posts
    where post_status = 'publish'
    and post_type = 'post'
    wp_posts 테이블에서 주의해야 할 점은 글의 리비전 정보까지 모두 이 테이블에 저장되기 때문에 post_status 컬럼이 publish 상태인 값만 조회를 해야 합니다. 그리고 post가 아닌 몇가지 다른 형태의 데이터도 이 테이블에 저장되는데 주로 'post', 'revision', 'attachment', 'page' 등의 타입이 있습니다.
  • 특정 글의 category  또는 tag 목록 가져오기
    1
    2
    3
    4
    5
    select a.term_id, a.name, a.slug, b.taxonomy
    from wp_terms as a
    join wp_term_taxonomy b on a.term_id = b.term_id
    join wp_term_relationships c on b.term_taxonomy_id = c.term_taxonomy_id
    where c.object_id = ?
    "?" 로 되어 있는 부분에 특정 글의 ID를 입력합니다. 이 질의의 결과는 다음과 같습니다. wordpress_category_tag 보시는 것 처럼 category 뿐만 아니라 tag도 같이 조회됩니다. 이 질의 결과 중에 taxonomy가 'category'인 것과 'post_tag' 인 것을 구분하면 글의 category와 tag 정보를 가져올 수 있습니다.
  • 특정 tag 또는 category에 해당하는 모든 글 조회
    1
    2
    3
    4
    5
    6
    7
    select a.*
    from wp_posts a
    join wp_term_relationships b on a.ID = b.object_id
    join wp_term_taxonomy c on b.term_taxonomy_id = c.term_taxonomy_id
    where c.term_id = ?
    and a.post_status = 'publish'
    and a.post_type = 'post'
    "?" 로 되어 있는 부분에 앞의 질의 결과로 나타난 term_id  값을 입력합니다. 이 질의 작성 시에도 post에 대한 상태와 type에 대해서 반드시 지정을 해야 합니다.

마치며

Popit 서비스를 리뉴얼 하는 도중에 몇번이고 WordPress의 기능을 그냥 사용하는 것이 어떨까 하는 고민을 했습니다. 이글이 공개되어도 여러 분들이 굳이 왜 개발했는지? 라고 물으시겠죠. 본문에서 밝힌 이유도 있지만 개발자로서 Go와 React 조합으로 실제 운용되는 서비스를 만들어 보고 싶은 마음도 컸습니다. 만들어 보니 쓸만할 것 같다는 생각이 들어 코드도 공개하였습니다.

다음 글에서는 이번 글에서 설명한 데이터 모델을 기반으로 Go API 서버 구성에 대해 알아보도록 하겠습니다.

주석

[1]: 처음 생각에는 한, 두페이지 조금씩 개선해보자는 생각으로 시작했는데 생각보다 작업에 많은 시간이 들어 갔습니다. 본문에서도 언급했지만 기존 서비스를 유지해야 하는 제약 사항 등으로 인해 고려해야 될 요소들이 많이 늘어난 이유 때문이기도 합니다.  새로 개발한 또 다른 이유는 https 로 전환 후 페이스북 좋아요 초기화 되는 문제때문이기도 합니다. 대부분은 해결했지만 문제가 발생했던 기간 동안에 노출된 일부 페이지의 경우 다른 페이지들과 다르게 처리를 해야 하는 이슈가 있었습니다. WordPress의 소스 코드를 수정하여 해결은 했지만 뭔가 아쉬움이 있어서...

[2]: 이 조항이 의무 사항인지는 모르겠지만 고민없이 그냥 따르기로 했습니다.


Popit은 페이스북 댓글만 사용하고 있습니다. 페이스북 로그인 후 글을 보시면 댓글이 나타납니다.