Single Page Application
내가 처음 SPA라는 것을 알게된게 작년 9월 쯤이었던 것 같다. 당시에는 자바스크립트에 대해 정말 무지해서 React
, Vue
같은 프레임워크들이 어렵게 느껴져 익히는 걸 미뤘었다. 그 핑계는 내 프로젝트엔 SSR
이 매우 중요하다는 것이었다. 언젠가는 이 프로젝트를 React
로 바꿔보고자 하였지만 그것은 기약없는 약속이었다.
최근이라 말하기엔 좀 그렇지만 1-2개월 전쯤 GitHub도 SPA
로 변경된 듯 보였고 점점 트랜디한 사이트들은 SPA
로 바뀌는 것 같았다. 슬슬 똥줄이 타기 시작했다. 더 미뤘다가는 기술 스택이 뒤쳐져 나락으로 떨어질 것 같은 불안감이 들었다. 그리하여 내가 이 프로젝트를 SPA
로 변경해야 만 하는 이유를 만들어 스스로를 채찍질 하였다.
- 나중에 백엔드 언어 바꾸는 상상을 해봐... 얼마나 편해지겠어?
- 아웃바운드 최소화 할 거라며? 지금이 그때야!
결심을 하고 React
의 SSR
을 정말 쉽게 만들어준다는 라이브러리 Next
에 대해서 알아보기 시작했다. 슬쩍 사용해보니 왠걸... 정말 쉬웠다. 디렉터리 구성도 내가 React
를 사용할 때 구성하던 것과 비슷했고 "아니 진짜 이렇게 쉽게 된다고?" 싶을 정도로 간단하게 SSR
이 구현된다.
클라이언트 코드에 Node
를 포함해서 개발한다고 생각하면 이해가 쉽다. Node
에서 생성한 props
을 컴포넌트에 던져주면 Next
가 SSR
을 해준다. 다만 그렇기 때문에 일부 라이브러리들은 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
나는 React
를 Class Component
기반으로 익혔고 그게 쉬웠다. 근데 요즘은 다 Functional Component
로 개발하는 것 같고 문법도 내가 잘 모르는 문법으로 작성되어 솔직히 한번에 이해하기 어렵더라. 근데 익히고보니 왜 이렇게 하는지 알겠다.
뭔가 클래스 기반의 컴포넌트는 새로운 컴포넌트를 만들기가 두려워지는데 함수형 기반의 컴포넌트는 그러한 두려움이 줄어들었다. 그래서 최대한 컴포넌트를 쪼개고 쪼개서 개발할 수 있었고 재사용성이 높아지는 것을 느꼈다.
Global State
로그인 상태를 관리하기 위해서 전역 상태를 관리할 필요가 있었다. 지금까지 React
는 기본적인 작업에만 사용해서 그냥 무한 props... 로 연명하고 있었는데 이렇게는 관리가 도저히 불가능 할 것 같았다. MobX
나 Redux
와 같은 라이브러리를 새롭게 익히고자 하였으나 러닝커브가 높아서 그냥 나만의 방식으로 처리했다.
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();
지금은 이런 방식으로 전역 상태를 관리하는데 아마 시간이 지날수록 복잡도가 올라가고 효율도 많이 떨어지게 될 것으로 예상된다. MobX
나 Recoil
같은 전역 상태 관리 라이브러리를 공부하여 업데이트 하도록 해야겠다.
FormData
POST
방식으로 new FormData()
에 파일 및 데이터를 담아 보낸 경우 장고의 request.POST
에 잘 계시는데 PUT
방식으로 보낸 경우 이상한 데이터가 전송되었다. 그래서 현재는 이미지 업로드는 가능하지만 이미지 수정이나 그런게 불가능하다. 이미지를 바이너리로 보내봤지만 처리가 번거로워서 이미지는 어떠한 이유로 요청을 보내던간에 POST
방식으로 처리하도록 하였다. 공부해서 개선해야지...
TypeScript
TypeScript
를 적용했는데 코드를 한동안 안봤다가 다시봐도 타입이 명시되어 있어서 수정하거나 새로운 것을 추가하기가 정말 쉬워졌다. 그동안 파이썬 같은 동적 타입의 언어를 선호했는데 이를 계기로 정적 타입의 언어들에 관심이 생기기 시작했다.
Deploy with Docker
Next
를 빌드하는 부분은 캐시를 전혀 사용할 수 없는걸까? 매번 실행시 빌드하는 시간이 상당히 아깝게 느껴진다. 개선할 수 있는 방법을 찾도록 해야겠다. 또한 프론트엔드 서버의 재시간 텀이 길어서 배포이후 몇초동안 502 페이지가 리턴된다. 해결할 방법을 고안하다가 Green Blue Deploy
라는 방식을 사용해서 무중단 배포를 적용하였다. 현재까지는 큰 문제없이 동작하고 있는 것으로 보인다.
Ghost