바닐라 자바스크립트로 SPA 만든 과정과 후기

개발

바닐라 자바스크립트로 SPA 만든 과정과 후기
최종 수정일:

굳이 바닐라 && SPA인 이유

근본의 바닐라

단순히 제 배경에 관한 이야기니, 관심 없으시면 아래 SPA / CSR부터 읽으시면 됩니다.

뜬금 없이 피아노에 관한 얘기로 시작해볼까 합니다. 보통 학원에서 바이엘 같은 기초적인 단계를 지나면 하농(Hanon)이란 책을 쥐여줍니다. 유튜브에 Hanon Exercise란 키워드로 검색해보시면 아시겠지만, 정말 아무 재미도 없이 손가락 바꾸는 연습만 종일 해야 하는 책입니다. 당시 초등학교 저학년쯤이었던 저는 굉장히 싫증 내며 몇 번이고 떼를 써서 이 책을 끝내지 않고 소곡집 등의 연주를 시작했습니다. 당장엔 훨씬 재밌는 곡을 치며 즐거웠는데, 곡이 어려워져가기 시작하니 손이 꼬여서 기교를 소화하는 데 집중하느라 음악적 표현은 하나도 못 하겠는 상황에까지 이르더라고요.
이때부터 기본기의 중요성을 몸소 깨닫고 근-본에 집착하기 시작했습니다.

거기에다 실행속도가 줄어드는 데서 어마어마한 희열을 느끼는 성향도 있기에, 근본 넘치는 바닐라를 선택하지 않을 이유가 전혀 없었습니다.

SPA(Single Page Application) / CSR(Client Side Rendering)

먼저, 블로그의 페이지엔 반복되는 요소가 정말 많습니다. 드로어부터 네비게이션, 푸터 등이 모든 페이지에 반복되고, 댓글 입력창, 헤더 구조 등이 포스트 페이지에서 반복됩니다. 여기에 상단 네비게이션에 있는 카테고리 목록과 최신 글 목록을 보여주는 요소같이 성능을 하락시키는 주범도 함께하기에, 서버가 매번 이것들을 보내주기보다 필요한 만큼의 정보만 가공해 클라이언트에게 던져주는 게 여러모로 합리적이라 생각했습니다.

다음으로, 다양한 웹사이트를 경험해보며 SPA가 주는 몰입감이 굉장히 마음에 들었습니다. 링크를 누르면 그 화면 그대로 잠깐 로딩하다 새 페이지를 툭 던져주는 기존 방식과 달리, 부드러운 애니메이션 등으로 페이지가 연결되는 느낌에 완전히 매료되었습니다.

마지막으로, 클라이언트는 별 하는 일도 없는데 애지중지하는 제 서버한테만 모든 일 떠맡기는 게 가슴이 아프다는 굉장히 주관적인 이유로 블로그를 SPA로 제작하기 시작했습니다.

SPA의 단점

위 점들에 이끌려 SPA로 작업을 시작했지만, 각종 라이브러리나 프레임워크로 제작하는 SPA는 단점도 분명히 존재하기에, 바닐라로 제작해 훨씬 세밀하게 최적화가 가능하니 단점은 최대한 버리고 싶었습니다.

1. 느린 첫 로딩

대부분 SPA는 클라이언트 사이드에서 렌더링을 진행합니다. 서버에서 렌더링하는 전통적인 구조와 달리, 클라이언트에서 렌더링을 진행하면 사용자는 렌더링에 필요한 프레임워크나 라이브러리가 로딩되기 전까진 빈 화면만 보고 있어야 합니다. 여기다 심지어 이런저런 외부 라이브러리를 덕지덕지 붙여 개발하는 경우가 많으니 체감되는 로딩 속도가 더 느려질 여지는 충분합니다.

첫 렌더링은 서버에서 진행하게 해 빈 화면만 봐야 하는 걸 해결했고, 바닐라로 개발해 라이브러리 몸집이 커지는 걸 자연스레 막았습니다. 난독화한 메인 파일은 68KB로, React + React Dom(128KB)보다 가벼운 건 물론이고, jQuery(87KB)보다도 가볍습니다.

2. 검색 엔진 최적화

상술한 것처럼 대부분 SPA는 클라이언트에서 렌더링하고, 검색 엔진 봇 중에 자바스크립트 엔진을 내장한 봇은 많지 않습니다. React 등에선 서버 사이드 렌더링도 가능하긴 하지만, 느립니다. 당연히 느린 로딩 속도도 검색 엔진의 감점 요인이고요.

상술했듯 전 첫 렌더링은 서버가 진행하도록 해 봇이 보건 사람이 보건 같은 화면을 볼 수 있게 했습니다.

3. 리소스 먹는 괴물

당연히 클라이언트가 일을 고전적인 방식보단 많이 해야 하기에, 해결하지 못한 단점입니다. 심지어 전 브라우저의 가비지 컬렉터(GC)에 많이 의존하게 작업해둬서 여러모로 구형 브라우저와 구형 기기에 작별인사하며 작업해야 했습니다.

제작 과정

백엔드

<?php
$request = $_REQUEST['json'];
if ($request) : header('Content-Type: application/json'); ?>
 
    <!-- JSON 응답 -->
 
<?php else : get_header(); ?>
 
    <!-- 기존 테마 코드 -->
 
<?php get_footer();
endif; ?>

워드프레스의 single.phparchive.php 등을 수정해 json 쿼리가 들어오면 json으로 응답하게 해뒀습니다. json_encode는 그냥 텍스트만 채워서 응답하는 것보다 느려서 그냥 json_encode등은 사용하지 않고 echo로만 꽉꽉 채웠습니다.
어레이(Array)에 아이템을 넣을 땐 마지막 아이템만 제외하고 마지막에 쉼표(,)가 들어가야 해서,

<?php
$array = '';
while ($recent->have_posts()) {
    $array .= '{"title" : "' . get_the_title() . '"},';
}
echo '[' . substr($array, 0, -1) . ']';

이런 식으로 작업했습니다. 짧아서 그나마 볼만한데, 아래에 응답 예시 사진을 첨부해뒀으니 한 번 참고해주세요. 저만큼 들어가면 여기서부터 살짝 헛구역질이 나오기 시작합니다.
따옴표나 역슬래쉬 등의 기호가 들어가면 올바르지 않은 json을 보내버리고, 클라이언트가 파싱하지 못하니 validator를 사용해 별 해괴한 문자 다 집어 넣어보며 수차례 검증을 진행해야 했습니다.

html 응답

일반적인 주소로 접근했을 때

json 응답

쿼리에 json이 포함되어 있을 때

프론트엔드

처음부터 모든 상황에 업데이트가 가능한 코드를 작성하진 못했고, 단계를 거쳐 가며 조금씩 SPA로 발전시켰습니다.

댓글

댓글 json

제일 처음엔 댓글의 html 코드만 따로 받아와 innerHTML로 통째로 덮어쓰는 방식을 쓰다가, PHP와 워드프레스에 관한 지식이 늘고 위 방식처럼 json으로 응답하게 업데이트했습니다.
댓글 하나가 추가 / 삭제될 수 있는 부분이라, 유일하게 객체를 저장해두고 객체를 비교해 돔을 업데이트하는 React 등의 가상 돔과 비슷한 구조로 만든 부분입니다.

비교 / 탐색을 좀 최적화하고 싶은데, 댓글 내용의 업데이트 여부도 감시하다 보니 일일이 기존 객체의 아이템과 새 객체의 아이템을 비교해야 해 묘수가 없는지 아직도 고민 중입니다.

포스트 -> 포스트

블로그의 모든 페이지는 홈, 아카이브, 포스트, 페이지 넷 중 하나입니다. 다른 타입으로 넘어갈 땐 업데이트해야 할 게 많아 제일 처음엔 포스트에서 포스트로만 넘어갈 때 클라이언트에서 렌더링하게 했습니다.

이 역시 단순히 json을 파싱하고, 글 내용을 통째로 업데이트하는 단순한 방식이었습니다. 여기서부터 기존 SPA와 다르게 훨씬 세밀한 최적화가 가능하다는 게 체감되기 시작했는데, 카테고리 이름만 비교해보고 '카테고리 이름이 같으면 여기서부터 이것까진 무조건 같다'거나, 카테고리 다른 글을 업데이트할 때 '주소가 같으면 나머지 내용은 무조건 같다' 등 어디서부터 어디까지 변하는지 알기에 훨씬 세밀하게 최적화가 가능했습니다.

마침내, SPA

document.querySelectorAll("a").forEach((element) => {
    addFetchEvent(element);
});
 
window.addEventListener("popstate", (event) => {
    callFetchJson(event, true);
});

참고: addFetchEvent는 a 태그에 target="_blank"등의 속성이 있는지 확인하고 클릭 이벤트에 callFetchJson을 추가하는 함수입니다.

블로그 구조

블로그의 html 모습입니다. 업데이트의 핵심은 선택된 main#main이기에, MainContainer란 class를 만들어 Main이란 변수에 할당해줬습니다. callFetchJson이 불러올 링크와 pathname + search 등이 같은지 판단해 json을 요청하는 fetchJson을 실행하면, response.status에 대한 확인, 애니메이션 등을 실행하고 Main.render에 response를 넘겨주면 넘겨받은 json을 바탕으로 updateArticle, createHome 등을 실행하게 해뒀습니다.
포스트처럼 같은 타입 간 이동이 있을 수 있으면 update를, 홈처럼 같은 타입 간 이동이 있을 수 없으면 create를 붙였습니다. update 시리즈엔 같은 타입인지 확인해 타입이 같을 때와 다를 때 다른 코드가 실행되게 해뒀습니다.

멀미 유발 코드

일례로 포스트가 아닌 페이지에서 포스트 페이지를 렌더링할 때 생성해야 하는 요소들을 가져와 봤습니다. 살짝 보고 있으면 어지럼증마저 유발할 수 있는 코드를 작성하며, JSX는 얼마나 편한 것인지 몸소 체험하게 해줍니다.
심지어 append나 prepend를 제대로 안 해도 오류를 잡아내지 못하기에, 돔이 제대로 업데이트되고 있는지 정말 눈에 불을 켜고 확인해봐야 합니다.

그래도 React와 비교했을 때 평균적으로 30%가량 우수한 속도로 스크립팅 + 렌더링 + 페인팅 등의 업데이트 과정을 마쳤습니다. 이것만으로도 충분하지 싶네요.

후기

개발에 관심이 있고 개발을 하는 사람이 늘어났고, 기기의 성능이 비약적으로 향상되며 개발의 모토가 '개발자가 힘들어도 유저와 성능이 무조건 우선'에서 'UX와 성능이 소폭 하락하더라도 개발이 편할 것'으로 옮겨간 것처럼 보입니다. CSS-in-JS등이 그 방증이라 생각하고요. 아, jQuery나 React부터가 그것을 방증하는 것일지도 모르겠네요.

'뭐가 어떻게 돌아가는지 모르겠지만 일단 내 예상대로 작동해주는 것'이나 '조금의 성능을 포기하더라도 내가 편한 것'들에서 벗어나 바닥부터 만들어보니 '무엇이 왜 생겼는가'에 관한 이해와 프레임워크와 라이브러리의 동작 방식에 관한 기본적 이해가 가능해졌습니다. 토 나온단 말을 거의 달고 살았지만, 개발자 도구에서 작업이 평균 1ms씩만 줄어들어도 행복하기도 했고요.

성능 최우선주의자거나 근본주의자라면 한 번 도전해볼 만한 작업이지 않나 싶습니다. 물론 어디까지나 제 블로그의 데이터 구조가 단순해서 해볼 만했을지도 모르는 일이지만요.

Report an issue