1인 웹개발 프로젝트 : 리팩토링

1인 웹개발 프로젝트 : 리팩토링

Single Page Application

내가 처음 SPA라는 것을 알게된게 작년 9월 쯤이었던 것 같다. 당시에는 자바스크립트에 대해 정말 무지해서 React, Vue 같은 프레임워크들이 어렵게 느껴져 익히는 걸 미뤘었다. 그 핑계는 내 프로젝트엔 SSR이 매우 중요하다는 것이었다. 언젠가는 이 프로젝트를 React로 바꿔보고자 하였지만 그것은 기약없는 약속이었다.



최근이라 말하기엔 좀 그렇지만 1-2개월 전쯤 GitHub도 SPA로 변경된 듯 보였고 점점 트랜디한 사이트들은 SPA로 바뀌는 것 같았다. 슬슬 똥줄이 타기 시작했다. 더 미뤘다가는 기술 스택이 뒤쳐져 나락으로 떨어질 것 같은 불안감이 들었다. 그리하여 내가 이 프로젝트를 SPA로 변경해야 만 하는 이유를 만들어 스스로를 채찍질 하였다.

  • 나중에 백엔드 언어 바꾸는 상상을 해봐... 얼마나 편해지겠어?
  • 아웃바운드 최소화 할 거라며? 지금이 그때야!

결심을 하고 ReactSSR을 정말 쉽게 만들어준다는 라이브러리 Next에 대해서 알아보기 시작했다. 슬쩍 사용해보니 왠걸... 정말 쉬웠다. 디렉터리 구성도 내가 React를 사용할 때 구성하던 것과 비슷했고 "아니 진짜 이렇게 쉽게 된다고?" 싶을 정도로 간단하게 SSR이 구현된다.

클라이언트 코드에 Node를 포함해서 개발한다고 생각하면 이해가 쉽다. Node에서 생성한 props을 컴포넌트에 던져주면 NextSSR을 해준다. 다만 그렇기 때문에 일부 라이브러리들은 Next용으로 따로 받거나 코드를 삽입하여 서버에서 동작하는 코드와 클라이언트에서 동작하는 코드를 호환되도록 작성해야 한다.



일단 기존에 존재하던 기능들을 쭉 나열해서 할 수 있을 것 같은 일부터 하나씩 진행했다. 그냥 출력만 되던 부분들이나 기존에 비동기 통신으로 구성되었던 부분은 쉽게 이식시킬 수 있었는데 포스트 작성이나 기존에 장고 템플릿에 의존적이던 코드, jQuery에 의존적인 라이브러리를 대체하거나 새로 구현하는 부분에서 상당한 시간이 걸렸다.


알게된 것


로그인

프론트엔드와 백엔드가 분리된 상태에서는 어떻게 로그인을 하며 유지하는지 몰랐는데 이 부분을 알게 되었다. 이론적으로는 알고 있었지만 분리된 상태에서 장고에서 어떻게 처리를 해줘야하는지 몰랐다. 장고로 풀스택 개발할때는 그냥 아래와 같이 명령어를 쳐주면 되니 그냥 그려러니 했었다.

auth.login(request, user)

그냥 이렇게 하면 된다는 걸 알았을 뿐 위 동작이 어떤 동작을 수행하는지 몰랐다. 위 동작은 클라이언트에게 set-cookie 헤더에 Session ID를 걸어주는 역할을 수행한다. (내 사이트의 패킷을 분석하며 알아낸 사실이었다.) 그래서 분리된 상태에서도 같은 메서드를 사용하여 로그인을 시켜줄 수 있었다.

또한 Session ID가 쿠키이므로 클라이언트에서 접근이 가능할 것이라 생각했으나 불가하다는 것을 새롭게 알았다. (클라이언트에서 로그인 상태를 파악하려다 알아낸 사실이었다.) set-cookie 헤더에서 HttpOnly로 설정된 경우 클라이언트에서 접근할 수 없다.

또한 nextjs에서 SSR을 할 때 쿠키를 활용하는 경우가 있는데 상위 도메인은 같으나 하위 도메인이 다른 경우 set-cookie시에 도메인을 지정하지 않으면 이 쿠키를 활용할 수 없게 된다. 이 경우 장고에서 아래 설정을 추가해야 한다.

SESSION_COOKIE_DOMAIN = ".domain.com"


CSRF 해제

CSRF@csrf_exempt 데코레이터를 통해 부분적으로 해제할 수 있는데 전역적으로 해제하고자 하였다. 원레는 백엔드에서 CSRF 토큰을 가져와서 처리하려 했으나 Django에서 템플릿을 렌더링하지 않는 이상 어려울 것 같았다. 물론 중간에 한 번 돌아가면 쓸 수 있을 것 같지만 그렇게까지 해야할까...? 여하튼 전역적으로 해제하려면

from django.utils.deprecation import MiddlewareMixin

class DisableCSRF(MiddlewareMixin):
    def process_request(self, request):
        setattr(request, '_dont_enforce_csrf_checks', True)

위와같이 코드를 작성하고 settings의 미들웨어에 추가하면 된다. 종종 전역적으로 처리할게 있다면 유용할듯 보인다.


withCredentials

타 사이트간에 요청이 불가한건 알았지만 CORS 헤더를 추가하면 끝인줄 알았는데 쿠키는 또 따로 해제를 해줘야 했다. 현재 비동기 통신 라이브러리로 Axios를 사용하는데 요청할때

axios({
    method: 'PUT',
    url: 'http://localhost:8000/api/apple'
    withCredentials: true
});

위와같이 보내면 쿠키도 함께 보낸다. 하지만 장고에서도 이를 허용해야 한다. (이것땜에 시간 진짜 많이 잡아먹었다. 😥)

CORS_ALLOW_CREDENTIALS = True


개선한 것


기존에 비해서 개선된 점이라면 서버에서 해오던 많은 것들을 클라이언트에게 미뤘다는 것이다(?) 사용자 입장에선 개선이 아닐 수 있지만 서버 입장에선 확실한 개선이며 서버의 개선은 사용자 환경을 개선한다고 생각한다. 훗날에는 이미지 변환 같은 작업도 떠밀어야지(?)

여하튼 이는 클라이언트의 코드를 백엔드의 코드처럼 구조적으로 짤 수 있었던 덕분이라 생각한다. 왜 프론트엔드 프레임워크를 도입하는지 jQuery가 점점 사라져가는지 약간은 알 것 같다.


마크다운 변환

이 블로그는 마크다운으로 글을 작성하는데 사용자가 글을 작성할때 보여지는 미리보기와 서버에 실제 저장되는 데이터가 약간씩 달랐다. 이는 내가 자바스크립트에 익숙하지 않았으며 글쓰기 자체가 라이브러리에 굉장히 의존적이라 쉽게 손을 대지 못했기 때문이었다. 미리보기와 같은 기능으로 해결하고자 하였으나 그것은 근본적인 해결책이 아니었다.

SPA로 변환하며 글쓰기와 같은 기능들은 일부분만 라이브러리를 사용하고 주도권은 어떻게든 내가 가지고자 하였는데 여기서 시간을 많이 소비했다. 기존에 사용하던 마크다운 파서(Parsedown)와 유사한 라이브러리를 찾는데도 어려움이 있었다. 여하튼 서버와 같은 결과물을 토출하기 위해서는 서버에서 같은 라이브러리를 사용하거나 같은 방법으로 파싱하도록 하는 것을 고민했는데 최종적으론 사용자가 파싱해서 보내는 것이 최상의 선택지라 생각되었다.

추후에는 백엔드에서는 마크다운 데이터만 관리하고 프론트엔드에서 전달받은 마크다운을 랜더링하는 방식을 적용할 예정이다.


글 자동 저장

이건 사실 개선했다기 보다는 불가피한 선택이었다. 기존 MPA로 제작된 경우 타 페이지로 이동하는 것을 막을 수 있어 사용자의 실수를 차단할 수 있는데 SPA로 변경한 경우에는 뒤로가기나 페이지 전환을 막는게 불가하고 다시 뒤로 돌아와도 남아 있는게 없다. 이런 경우를 대비해서 임시저장을 최대화하여 글이 날아가는 현상은 막아야 했다. 다만 모바일에서 글을 작성하는 경우 무의미하게 데이터가 낭비된다고 생각하여 사용자가 자동저장을 켜고 끌 수 있는 기능을 추가하였다.


Functional

나는 ReactClass Component 기반으로 익혔고 그게 쉬웠다. 근데 요즘은 다 Functional Component로 개발하는 것 같고 문법도 내가 잘 모르는 문법으로 작성되어 솔직히 한번에 이해하기 어렵더라. 근데 익히고보니 왜 이렇게 하는지 알겠다.

뭔가 클래스 기반의 컴포넌트는 새로운 컴포넌트를 만들기가 두려워지는데 함수형 기반의 컴포넌트는 그러한 두려움이 줄어들었다. 그래서 최대한 컴포넌트를 쪼개고 쪼개서 개발할 수 있었고 재사용성이 높아지는 것을 느꼈다.


Global State

로그인 상태를 관리하기 위해서 전역 상태를 관리할 필요가 있었다. 지금까지 React는 기본적인 작업에만 사용해서 그냥 무한 props... 로 연명하고 있었는데 이렇게는 관리가 도저히 불가능 할 것 같았다. MobXRedux와 같은 라이브러리를 새롭게 익히고자 하였으나 러닝커브가 높아서 그냥 나만의 방식으로 처리했다.

class Global {
    constructor() {
        this.state = {
            shareVal: ''
        }
        this._updater = {};
    }

    setState(newState) {
        this.state = newState;
        Object.keys(this._updater).forEach(key => {
            try {
                this._updater[key]();
            } catch(e) {
                delete this._updater[key];
            }
        });
    }

    appendUpdater(name, fn) {
        this._updater[name] = fn;
    }

    popUpdater(name) {
        delete this._updater[name];
    }
}

export default new Global();

지금은 이런 방식으로 전역 상태를 관리하는데 아마 시간이 지날수록 복잡도가 올라가고 효율도 많이 떨어지게 될 것으로 예상된다. MobXRecoil 같은 전역 상태 관리 라이브러리를 공부하여 업데이트 하도록 해야겠다.


FormData

POST 방식으로 new FormData()에 파일 및 데이터를 담아 보낸 경우 장고의 request.POST에 잘 계시는데 PUT 방식으로 보낸 경우 이상한 데이터가 전송되었다. 그래서 현재는 이미지 업로드는 가능하지만 이미지 수정이나 그런게 불가능하다. 이미지를 바이너리로 보내봤지만 처리가 번거로워서 이미지는 어떠한 이유로 요청을 보내던간에 POST 방식으로 처리하도록 하였다. 공부해서 개선해야지...


TypeScript

TypeScript를 적용했는데 코드를 한동안 안봤다가 다시봐도 타입이 명시되어 있어서 수정하거나 새로운 것을 추가하기가 정말 쉬워졌다. 그동안 파이썬 같은 동적 타입의 언어를 선호했는데 이를 계기로 정적 타입의 언어들에 관심이 생기기 시작했다.


Deploy with Docker

Next를 빌드하는 부분은 캐시를 전혀 사용할 수 없는걸까? 매번 실행시 빌드하는 시간이 상당히 아깝게 느껴진다. 개선할 수 있는 방법을 찾도록 해야겠다. 또한 프론트엔드 서버의 재시간 텀이 길어서 배포이후 몇초동안 502 페이지가 리턴된다. 해결할 방법을 고안하다가 Green Blue Deploy 라는 방식을 사용해서 무중단 배포를 적용하였다. 현재까지는 큰 문제없이 동작하고 있는 것으로 보인다.

이 글이 도움이 되었나요?

신고하기
0분 전
작성된 댓글이 없습니다. 첫 댓글을 달아보세요!
    댓글을 작성하려면 로그인이 필요합니다.