brunch

You can make anything
by writing

C.S.Lewis

by 핑크곰 May 18. 2022

14F 서비스 구축

the14f.com

꽤나 맘에 들었던 콘텐츠를 보고 나서 기대감에 글을 적었더랬다.


예상했던 대로 14F는 안정적으로 자리를 잡았고, 4년 만에 꽤나 영향력 있는(현재 유튜브 약 140만 구독자) 뉴스 콘텐츠가 됐다. 내용은 보다 다양해졌고 지속할 수 있는 채널이 생겼다. 14F는 단순한 콘텐츠 제공자에서 서비스 플랫폼으로 변할 준비를 마쳤고, 뉴스레터 중심의 플랫폼을 개발하기에 이르렀다.


그런데 말입니다. 어쩌다 보니 필자가 그 서비스의 PM 및 개발을 하고 있는 게 아닌가... -_ -

물론 위의 글은 '1'도 영향을 미치지 않았다. 그냥 어쩌다 보니, 흘러가다 보니 그렇게 되더라.


... 그리고 고생했다. 그 이야기를 해보려고 한다.






2021년 초, CODDAS의 유튜브 데이터 수집 모듈을 마무리해가는 시기에 14F의 모바일 서비스에 대한 요청이 있었다.(아... 리프레시 휴가 ㅠㅠ) 팀에서는 14F 플랫폼 서비스를 긍정적으로 바라보고 지원을 하기로 결정했다. 2개월(2021년 4월~5월) 동안 파일럿을 진행한 뒤 정식 서비스(모바일 앱, 웹 서비스)를 개발했다.




파일럿


앱은 안드로이드, iOS 프로젝트를 동시에 관리할 수 있도록 Flutter를 사용하기로 결정했고 두 명의 개발자가 투입됐다. Flutter의 유지보수가 걱정되긴 했지만, 경험상 안드로이드, iOS 플랫폼을 각각 유지보수 하는 것보다 하나로 가는 게 더 좋다고 판단했다.(신기술을 익히는 게 재밌고 즐거워서가 아니다. 뜨끔.. 흠흠;;;)


필자는 CMS(Contents Management System)와 Backend API 개발을 책임졌다.

시간과 비용의 제약사항 덕분(?)에 오픈소스 CMS를 활용하기로 했다. 콘텐츠와 사용자를 관리할 수 있는 검증된 CMS가 필요했고 API를 유연하게 사용할 수 있는지가 플러스 요인이었다. 추가로, 콘텐츠를 브라우저로 보여줄 게 아니기 때문에 Headless CMS도 대상에 넣었고, Backend API와 연동을 고려해서 NodeJs 기반 프로젝트도 염두했다.


레퍼런스가 다양하고 확장에 유연한 프로젝트 중심으로 리서치한 결과, 아래 3개의 솔루션이 눈에 들어왔다.

- Wordpress : 시장 점유율 1위, 소규모 서비스에선 국룰

- Strapi : NodeJs 기반의 Headless CMS 대표주자

- Ghost : NodeJs 기반의 종합 CMS


고민 끝에, 플러그인 마켓 규모가 크고 국내외 레퍼런스가 충분한 "워드프레스"를 사용하기로 결정했다.(뒤에서 언급하겠지만 Headless CMS를 선택하지 않은 게 정말 다행) 사실, 워드프레스가 PHP 기반이라 좀 꺼려졌지만(필자가 PHP 3~4를 취미로 사용한 고인물이라 PHP에 대한 신뢰가 없다.  -_ -;;) 강력한 개발자 커뮤니티를 포기할 수가 없었다.


앱을 위한 Backend API는 최근에 진행했던 프로젝트를 활용해서 NodeJs 기반으로 개발했다. 회원 관리 부분을 제외하고 복잡한 알고리즘이 없었기 때문에 대부분 워드프레스의 API를 핸들링하고 보안 처리 및 성능을 위한 캐싱 역할을 수행했다.




구독형 웹 서비스, the14f.com


파일럿 보고 이후, 정식으로 프로젝트를 진행했다. CMS를 직접 만들지 않는 게 얼마나 다행이냐며 자위하며 개발을 하던 찰나, 청천벽력 같은 요구사항이 들려왔다.


'앱 뿐만 아니라 메일 구독을 위한 웹 서비스도 필요합니다.'


이러워이;ㅏㅓㄹㅇ;ㅣ이ㅏ러미ㅑㅇ럼;ㅐ덜;메ㅐㅑ덜;미ㅑ얼


'웹 서비스는 외부 업체를 통해 개발하니 앱 개발과는 별개다'라고 했지만, 뭐 그게 말처럼 되나... RFP 작성부터 개발업체 선정까지 제대로 진행이 안 되는 상황에 직면했다. 결국 불을 끄러 PM으로 투입.


시간이 많이 지체된 탓에, 최대한 빠른 시간 내에 워드프레스에 경험이 많은 개발 업체를 찾아서 레퍼런스 체크를 진행하고 선정했다. 그런데, 워드프레스 웹 퍼블리싱에 최적화된 회사이다 보니 상대적으로 개발팀 문제가 부각됐다.


큰 이슈는 메일 서비스 쪽이었는데, smtp.com과 mailjet 서비스 때문에 많은 시간과 노력을 허비한 탓에 프로젝트가 오랜 시간 지체됐다. 결국 비용과 안전성 그리고 국내 메일 서비스 사례를 검토하여 AWS SES로 교체했다.


제일 암울했던 건, 파일럿에서 AWS Cloud Formation 기반으로 구성한 서버 아키텍처를 오픈을 앞두고 제대로 구성하지 못해서 오픈 일정이 연기됐다. 


https://github.com/aws-samples/aws-refarch-wordpress



결국 뉴스 프로젝트를 맡고 있는 클라우드 관리 업체를 통해 문제를 해결했지만, 이번에는 워드프레스의 배치 작업이 RDS의 CPU와 저장공간을 너무 많이 잡아먹는 문제가 발생, 이래저래 험난한 일정이었다.


우여곡절 끝에 열심히 불을 끄고(이런 PM은 다시는 하고 싶지 않음;;;) 2월 말에 서비스를 정상적으로 오픈했다.




모바일 앱


모바일 앱은 파일럿 내용에서 크게 변화 없이 진행했다.
다만, 파일럿에서 콘텐츠 관리를 위해 사용하던 플러그인 대신 웹 서비스에서 다른 플러그인을 사용한 탓에 기존에 사용하던 워드프레스용 API를 개발업체에서 커스트마이징해서 제공했는데, 이 API 때문에 여러모로 고생을 많이 했다. 워드프레스 API를 활용한 프로젝트에서는 API를 임의로 수정하기보다는 성능을 비롯해 유지보수 관점에서 그대로 사용하는 것을 권장하고 싶다.


그리고 소셜 로그인(카카오, 네이버, 애플, 구글) 기능이 추가됐다. Backend에서는 각 소셜 플랫폼 API를 활용해서 인증 검증을 진행했다. iOS의 경우 소셜 로그인 기능이 들어갈 경우 반드시 애플 로그인 기능이 존재해야만 앱 심사에서 통과를 한다. 그런데 이 애플 로그인이 타 SNS 로그인 API와 다르게 OAuth2 기준을 따르지 않아 여러모로 고생했다.


그밖에, 앱의 많은 부분에서 Firebase를 활용했다. 푸시 발송을 위한 Cloud Messaging, 이메일 앱 구동을 위한 Dynamic Links, 앱 모니터링을 위한 Crashlytics/Performance 등이 사용됐다.(Fabric 사용하던 게 엊그제 같은데 이렇게 다 뭉쳐있으니 기분이 묘하다.)


베타 테스트를 마치고 5월 16일 드디어 앱 서비스를 오픈하면서 약 1년간의 프로젝트를 마무리할 수 있었다.




프로젝트 소고(기술 관점)


워드프레스

14F의 콘텐츠 항목과 워드프레스가 제공하는 콘텐츠의 기본 구성이 상이했기 때문에, 이를 커스트마이징 할 수 있는 기능이 필요했다. 이를 위해 Pods 플러그인을 사용했는데 꽤나 만족했다.

 API 확장에 매우 유연했고 데이터의 백업 및 이관 작업도 유용했다.


워드프레스는 공식으로 지원하는 REST API가 존재한다. 대부분의 플러그인들은 이 API를 활용하기 때문에 별도의 API를 제공하는 경우는 드물다.


워드프레스의 웹 화면에서는 쿠키 방식으로 인증을 처리하지만, REST API는 인증 방법을 제공하지 않는다. 때문에 앱에서는 로그인 세션을 쿠키로 얻어서 사용하거나 별도의 인증 플러그인을 사용해야 한다. 앱에서 쿠키 핸들링이 번거롭기 때문에 별도의 인증 플러그인(Basic-Auth)을 사용했다.


앱에서 워드프레스의 포스트(글)를 그대로 활용하려 했는데 반환 데이터가 HTML 안에 녹아들기 때문에 사용에 문제가 있었다.(웹뷰로 보여주는 경우가 아니라면 파싱 예외들이 많을 수 있음) 결국 Pods의 Custom Post Type을 활용하는 게 관리, 개발 측면에서 모두 유용했다.(Custom Taxonomy를 통해서 카테고리나 태그 같은 분류가 임의로 추가 가능하고 기존 POST의 필수 필드(제목, 내용)를 무시할 수 있음)


푸시 서비스

워드프레스에 푸시 메시지를 관리하는 전용 메뉴와 콘텐츠 별로 글을 작성하면 푸시가 발송되는 기능도 추가했다. 푸시 시스템과 관련된 로그 데이터는 별도의 DB 테이블을 활용했다.


푸시 메시지를 작성하고 워드프레스의 후킹 처리로 푸시를 발송하려 했다. 그런데 Pods로 텍소노미를 추가하고 라디오 버튼으로 변경하니 구텐베르크 에디터에서는 푸시 텍소노미가 안 보이는 문제가 발생하더라;; 때문에, 각 포스팅 타입에 푸시 발송 여부 필드를 추가했다.


구텐베르크 에디터에서 후킹이 두 번 불리는 문제 발생했다. 구글링을 통해 아래 방법으로 해결했다.

if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) { 
   return;
}

 

워드프레스에서 Pods를 사용한 콘텐츠의 후커를 처리할 경우, 기본 후커(ex, save_post)에서는 Pods의 커스텀 데이터를 가져올 수 없다. Pods 데이터가 후킹 시에는 생성 안됐기 때문이다. 때문에 Pods에서 제공하는 후커(ex, pods_api_post_save_pod_item)를 사용해야 한다.


예약 배포를 할 경우, pods_api_post_save_pod_item 후커는 예약 발송을 입력하는 시점에서 호출된다.

즉, 예약된 배포 시각에 호출되는 것이 아니라, 예약을 저장하는 시점에 호출되기 때문에 예약 시간을 설정한 경우 기존의 Pods  후커 대신 예약 시각에 호출되는 워드프레스의 후커(save_post)를 추가해야 한다.(다행히 이 시점에는 위에 언급한 Pods의 데이터가 보이지 않는 문제는 해결된다. 이미 저장이 된 상태이기 때문에) 아래와 같이, 기존 Pods 후커에 status 값이 future 인 경우 처리하지 않도록 추가하고, 

if (in_array( $post->post_status, array( 'private', 'trash', 'auto-draft', 'draft', 'future' ), true ) ) {
    return;
}   

save_post 후커에서 중복 발송을 막기 위해 date, modified 필드를 비교하여 예외처리했다.

$modified_date = new DateTime($tmp_post_obj->post_modified);
$publish_date = new DateTime($tmp_post_obj->post_date);
if ($publish_date <= $modified_date) {
    return; 
}                  


메일 서비스

대용량 메일 발송 시 도메인의 검증 여부에 따라 발송이 정상적으로 되지 않거나 스팸으로 처리되는 경우가 있다. SPF 레코드, PTR 레코드 등록, 화이트 도메인 등록은 필수(참고 : https://m.blog.naver.com/PostView.naver?isHttpsRedirect=true&blogId=hilineisp&logNo=220107284167)


해외 메일 서비스를 사용할 때는 반드시 주변에 적용 사례가 있는지 확인해 보기 바란다. 개인적으로는 비용, 성능, 서비스를 고려할 때 AWS의 SES를 추천한다. 단, SES의 경우 주의해야 할 점이 메일 수신율이 떨어지면 차단당하고 메일로 풀어달라고 요청해야 한다.(테스트를 위해 임시로 만든 가짜 메일 때문에 차단당해서 고생했던 아픈 기억이.. ㅠㅠ)


캐시 서버

캐시 서버를 적용할 경우, 개발 및 테스트 시점에 디버깅이 어려울 수 있으니(특히 View Tier) 캐시 조회, 삭제 등의 개발용 API를 추가로 제공해 주는 것이 좋다.

브런치는 최신 브라우저에 최적화 되어있습니다. IE chrome safari