spaceBetweenWorkAndRest
Next js에서 안전하게 상태 관리 하기 - 2021.10.04 본문
시작하기 전에...
우선 글을 시작하기 전에 한가지 말을 먼저 하자면, 이 글에서 다룰 내용은 여러분이 이미 알고 있을 내용일 수도 있고, 정답이 아닐수도 있다는 말을 드리고 싶다. 이 글은 주니어 개발자의 좌충우돌 상태 설계 **시도(?)**이기 때문이다. 나름 스스로 오랫동안 고민하고 다양한 레퍼런스를 조사하고 나서 개인적으로 가장 합리적이라고 생각하는 아키텍쳐를 선택하게 되었다.
Next js의 렌더링 사이클
우선 상태 관리를 하기 전에 next js에서의 렌더링 사이클에 대한 이해가 필요하다. 기본적으로 next js는 ssr(server-side-render)과 csr(client-side-render)을 동시에 지원하기 때문에 page간 전환 시 어떤 흐름으로 렌더링이 되는지 개인적으로 많이 혼란스러웠다.
next js는 spa(single-page-application)와 달리 진입점이 한곳이 아니기 때문에 여러가지 진입점에서 라우팅을 할 때 기존의 spa와는 렌더링 흐름이 다르다.
또한 어떤 로직은 서버 사이드에서 실행되고 어떤 로직은 클라이언트 사이드에서 실행되기 때문에 상태 관리 로직을 작성하기 전 next js에서의 렌더링 사이클을 정리해야만 했다.
아래는 간략하게 도식화 한 next js에서의 렌더링 사이클이다.
- 우선 눈여겨볼 곳은 클라이언트가 매번 라우터에 진입할 때 서버에서 비즈니스 로직이 한번 동작하는 파란 부분이다. next js에서는 해당 url에 유저가 접근할 때 마다 항상 서버에서 비즈니스 로직이 동작한다. 현재 next js에서 권장하는 비즈니스 로직을 담당하는 함수는 크게 getServerSideProps, getStaticProps가 있다.
- 두번째로 눈여겨볼 점은 클라이언트 사이드에서 동작하는 빨간 부분이다. 이 부분이 특이한 점은 next js에서 권장하는 routing 방식을 사용하면*(next/router)* 두 컴포넌트가 상태를 공유할 수 있다는 점이다. 즉 기존에 불러온 route의 js파일은 두번 불러오지 않는다는 말이고 client side render를 실현할 수 있다는 말이다.
- 마지막으로 중요한 부분은 이미 방문한 route의 js파일은 다시 불러오지 않지만, 파란부분의 로직은 빨간 부분의 로직과는 별개로 이미 방문한 route일지라도 항상 실행된다는 점이다.
이러한 특징 때문에 next js는 서버 사이드 렌더링의 이점을 취하면서도, 클라이언트 사이드의 빠른 페이지 업데이가 가능하다는 장점이 있다.
그래서 문제는 무엇이었나?
첫번째 문제
문제는 간단하지만 해결하긴 까다로웠다. 결국 클라이언트 사이드에서 상태 관리를 하기 위해선 클라이언트 사이드의 진입점인 _app.js에서 전역 상태를 관리해야만 했는데 이럴 경우 아래와 같은 우려 사항이 있었다.
- _app.js에서 전역 상태 관리를 시작한다면 기존의 spa방식과 같아서 next js의 이점을 전혀 살릴 수 없었다. 그 이유는 서버에서 동작하는 비즈니스 로직은(getServerSideProps) client와 완전히 독립적으로 동작하기 때문에 client의 상태에 접근할 수 없었다.
- 컴포넌트 간 global store를 공유하고 있기 때문에 여러개의 store를 생성할 수 없었다. 이 말은 마찬가지로 spa와 다를바 없는 next js프로젝트가 되어버리고, 여러개의 store를 생성할 수 없다는 점은 모든 route에서 해당 route가 사용하지 않는 불필요한 상태까지 initialize해야하기 때문에 불필요한 상태들 까지 관리해야 한다는 불합리함이 있었다.
- 또한 _app.js안의 로직은 매 route 변경시 항상 실행되는 로직이기 때문에 이곳에서 routing으로 인해 로직이 무거워 진다면 전체적인 앱의 성능또한 우려되는 상황이었다.
위와 같은 문제를 해결 할 방법은 없을까?
첫번째 문제의 해결
답은 '있다' 이다.
안타깝지만 _app.js에서 로직을 작성해서 전역 상태를 관리해야 한다는 문제는 피할 수 없었다. 따라서 _app.js의 로직이 기존에 비해 다소 무거워 질것은 감안 해야만 했다. 위에 작성한 세번째 문제는 피할 수 없었다. 다만 첫번째, 두번째 문제를 완화할 수 있는 방법을 생각했다.
- server에서 '**client의 상태에 접근할 수 없었다'**는 문제는 next-redux-wrapper 라이브러리를 많이 참고했다. 서버에서는 당연히 클라이언트의 상태에 접근할 수는 없다. 하지만 상태를 주입할 수는 있다. 접근과 주입은 분명히 다르지만 필요한 동작을 구현하기엔 주입만으로 충분했다. 접근은 흔히 말하는 read only인 것이고, 주입은 write only인 상태인데, routing을 할 때는 사실 상 해당 route에 대해 필요한 상태를 서버에서 받아와 주입만 하면 되고, 접근할 필요 까지는 없을 것이다. 이렇게 접근과 주입을 다르게 생각하면 충분히 서버 사이드 렌더링의 이점을 취하면서 전역 store를 활용할 수 있다.
- '**불필요한 상태들 까지 관리해야 한다는 불합리함'**이라는 부분 또한 _app.js에서 분기 처리를 해 각 컴포넌트에 필요한 상태 까지만 initialize하면 된다. 이 역시 상태를 주입한다는 개념에 의하면 충분히 완화 할 수 있는 부분이었다.
문제를 해결(?)한 상태에서 다시 상황을 도식화 한다면 아래와 같다.
두번째 문제
만세! 문제를 해결했다. 이제 각 컴포넌트에서 store에 접근해서 필요한 상태를 쓰기만 하면 된다. 상태 관리 library는 redux가 아닌 mobx를 선택했기 때문에 flux pattern이 아니라서 불필요한 코드 없이 쉽게 구현할 수 있겠다! 작업을 시작...
근데 잠깐... 컴포넌트에서 상태에 접근한다고? 컴포넌트는 접근 뿐만 아니라 상태를 수정해야 할 수도 있는데...
예로부터 view와 model을 서로를 모르게 하려고 다양한 아키텍쳐가 나왔는데, 현재 상황은 view가 model에 직접 접근하는 완전히 시대를 역행하는 방법이었다. 서버 혹은 기획의 변경으로 store의 상태가 변화한다면? 모든 컴포넌트가 영향을 받게 된다. 작업을 시작하기도 악취가 났기 때문에 이 상황을 해결해야만 했다.
위와 같은 문제를 해결 할 방법은 없을까?
두번째 문제의 해결
답은 '있다' 이다.
우선 가장 먼저 떠오른 방법은 flux pattern이었다. 이미 redux 사용에 익숙하기도 했고, 상태 관리에 있어 번거롭지만 안전하게 view와 model을 분리할 수 있는 꾸준히 사랑받아온 패턴이기 때문이다.
하지만 이는 우리가 상태 관리를 하기 위해 mobx를 선택한 이유와 완전히 반대되는 이유였다. 기존의 flux 패턴의 단점을 그대로 다시 가져가야 한다는 불합리함이 있었다. 다른 방법은 없을까?
다른 방법으로는 view-model을 사용하는 mvvm pattern이 있다. vue.js에서도 사용하는 이 패턴은 마찬가지로 현재 가장 사랑받는 view와 model을 분리하는 패턴중 하나이다.
mvvm 패턴을 적용한다면 이제 컴포넌트가 store를 몰라도 되는 구조를 잡을 수 있었다. (no flux!) 이를 적용한다면 안전하게 컴포넌트를 관리할 수 있을거 같다.
view model까지 적용한 상태를 도식화 한다면 아래와 같다.
문제가 모두 해결! 되었나(?)
글을 마무리 하며...
서두에 말했다시피 이 글은 주니어 개발자의 좌충우돌 상태 설계 시도 이기 때문에 위 패턴은 정답이라고 볼 수 없다. 다만 앱은 구현 되어야만 하고, 더 이상 설계에 시간을 사용할 수 없어서 개인적으로 가장 합리적이라는 방법 까지만 생각해 두었다. 앞으로 나의 숙제는 위 같은 방법으로 앱을 구현했을 때 어떤 문제를 초래할 지에 대해 관심있게 지켜 보아야 할것이다.
세번째 문제가 나오지 않기를 바란다.