불완전한 걸 싫어하는 성격이라 기록을 남기는 데 지나치게 신중해 왔다. 때문에 과거는 흔적조차 없을거라 생각했다. Gardener와 White Places라는 실패한 프로젝트의 기록을 내가 과연 남겨 뒀을까? 하지만 전역하고 나서 복학을 준비하기 위해 Notion에 접속했을 때, 생각보다 많은 기록이 남아있어 놀랐다. 여기저기 흩어져 사용할 수 없는 상태였기 때문에 학기 중 틈틈이 정리하기로 했고 그 작업이 드디어 끝났다.
재밌게도 시간이 흐르면서 글의 성격이 바뀌었다. 초창기의 글(gardener)은 사실을 정성 들여 정리한 내용이다. 출처도 명확하고 이모티콘도 가득하다. 다만 깊이 있는 내용보다 작은 개념이 주류다. 이후 글(white places)은 점점 혼잣말이 된다. 이 유형이 가장 기분 좋았는데, 당시 내 모습을 생생하게 떠올릴 수 있었기 때문이다. 내가 어떻게 이해하고 있는지, 어떤 관점으로 문제를 바라보고 있는지 가장 잘 드러나 있다. 과거 기록이다 보니 잘못 정리된 내용도 꽤 있는데 이 유형의 글은 내가 왜 잘못 이해했는지 눈에 보여 감상에 잠기기도 했다. 그리고 최근 글(전역 후)은 사실을 정리한 것인데 다시 읽기 위해서가 아닌 공부하는 내용을 더 오래 기억하기 위해서 정리했다는 점에서 초창기의 글과 대조적이다.
기록을 정리하다 보니 내 성격이 정말 잘 드러나 있다. 한 문장으로 표현하면 "옆길로 잘 새는 사람"이랄까? 성격을 토대로 과거 두 프로젝트 실패 원인을 분석해 보았다.
인생이 선택의 연속이듯 개발도 선택의 연속이다. 어떤 API를 디자인할 것인가? yes 버튼은 오른쪽에 있어야 할 것인가 왼쪽에 있어야 할 것인가? 중요한 점은 선택은 곧 책임이고 반드시 근거가 있어야 한다는 것이다. 가령 충돌이 거의 발생하지 않는 환경에서 비관적 lock을 선택했다면 최소한 "아직 Application level에서 낙관적 lock을 구성해야 할 정도로 성능이 문제 되지 않는데, 비관적 lock을 구현하는게 더 익숙해 개발 속도를 올리고 싶었다"와 같은 이유라도 있어야 한다.
조금 더 정확히 표현하자면, "처음부터 완벽"하려 했다. 여기서 더 정확하게 얘기하자면 "겁이 많고, 게으르다"고 표현할 수 있다. 스스로를 너무 부정적으로 표현하는게 아닌가 싶을 수 있지만, 일단 지금은 과거 실패 원인을 고민해보는 시간이다.
한 번 만들면 수정하기 싫었고 남에게 보이려면 뼈대가 견고하길 바랐다. 그래서 문서 작업도 정말 열심히 했다.
돌아만 가는 코드는 싫다. '구조적으로 문제없는' 프로그램을 만들고 싶었다. 가령 사용자 정보를 DB에 저장할 때, 기업이 정보를 열람할 수 없도록 할 수는 없을까? password에 salt를 더하면 기업도 passowrd를 알 수 없다. 다만 이 경우는 data를 서비스가 보관하는게 아니라 사용자가 가지고 있다. (지금 든 생각인데, id는 찾아줘도 password는 재발급하는 이유는 salt 때문이 아닐까?) 하지만 내가 원하는 건 hash가 아닌 암호화다. 사용자가 가지고 있는게 아닌 서비스가 가지고 있는 data를 기업으로부터 지킬 순 없을까? 가장 간단한 방법은 end-device에서 암호화 한 정보를 서버로 보내는 것이다. 하지만 multi-device를 지원해야 한다면? 암호키를 사용자가 보관해야 한다. 가능은 하지만 과연 대중이 불편함을 감수할까? 실제 서비스는 어떤 방식으로 동작할까?
선택의 근거를 찾는 건 쉽지 않고 시간을 갈아 넣으며 공부해야 한다. 새로운 기술이 더 뛰어난 성능을 보이더라도 그 기술을 익히고 기존 기술에서 이주하는데 아주 큰 노력이 필요하다. 그래서 legacy가 존재한다. 당시 나에게 legacy는 '악'이었다. 그래서 선택의 순간에 잘못된 판단을 내리게 됐다.
api를 설계할 때, restFul api를 선택할 근거가 필요했다. 조금 알아보다 graphQL을 알게 됐다. 아무리 생각해도 graphQL이 옳았다. 그래서 graphQL을 새로 공부했다. 함수형 패러다임을 접했다. 쓰지 않을 이유가 없었다. Elm을 공부했다. express를 공부하다 nest를 알게 됐다. 써보기로 한다. nest는 decorator pattern을 쓴다. 적용을 시도하지만 (아마 typescript 호환 문제로) 실패한다. graphQL schema를 .d.ts
로 만들어 주는 code generator를 도입한다. elm 라이브러리가 제대로 동작하지 않는다. 결국 react로 돌아온다. react-router의 파일 구조가 마음에 들지 않는다. (당시) beta인 v5를 사용한다. 호환 문제로 다시 다운그레이드한다.
공부하다 보면 약간의 해답과 다수의 질문을 얻는다. 나는 이 과정에 영리하지 못했다. 새로운 기술은 좋다. 하지만 내가 그것을 소화할 수 있을지부터 확인해야 했다.
create-react-app
이 싫다. 내가 알지 못하는, 관리할 수 없는 build는 끔찍했다. 그래서 webpack을 가져다 직접 개발 환경을 만들었다. hot-reloading이 hot-reloading이 아니게 되어서 코드 한 줄 바꾸면 8초를 기다려야 했다. 아마 설정 지옥에 빠져버린 건 거기서부터지 않았을까?
eslint랑 prettier 문서를 왜 그렇게 열심히 봤을까? decorator와 같이 무언가 새로운 걸 도입하려고 preset을 이것저것 추가했다 삭제했다 반복했다. husky로 local git hook을 설정했고 TDD를 해보겠다고 mocha/chai를 깔았다 jest를 깔았다 반복했다. 설정 파일의 가독성을 올리겠다고 rc파일 기본 형태를 .yaml로 바꿨다. toml은 그때 처음 봤다. (그 뒤로도 toml을 쓰는 다른 프로그램을 본 적이 없다)
앞으로는 뭐가 됐든 최대한 간소하게 해보려 한다. 모든 면에서 완벽하면 좋겠지만, 시간은 기다려 주지 않으니까. 필요한 만큼만 있으면 충분하니까
위 언급했던 중간 단계의 기록을 보면 당시 고민하던 내 모습이 남아있다. 여기서부턴 그 기록 중 일부를 공유해 보려 한다.
컴포넌트가 반드시 '어느 상황'에나 사용 가능할 필요는 없지 않을까? 어차피 '내 프로그램'에 사용될 컴포넌트다. RWA에는 navBar가 component로 분류된다. page가 아니라. 하지만 로고 등을 입력으로 받지 않는다.
이제껏 '/'과 '/project'를 비롯한 모든 page에서 header와 같은 공통 UI가 존재한다면, app.tsx에서 해당 UI를 렌더링하고, 내부 Route로 나머지를 렌더링하는게 옳다고 생각했다. 하지만 RWA는 다른 방식으로 접근했다. MainLayout이란 컴포넌트는 children을 header와 함께 렌더링하도록 하는 일종의 Wrapper다. 따라서 app.tsx에서는 routing만 진행하고, 각각의 page는 mainLayout wrapper를 사용해서 디자인된다. 이 방법이 훨씬 깔끔하지 않을까?
컴포넌트는 일부일 뿐이며, 때문에 내 프로젝트에 종속적이면 안 된다고 생각했다. 하지만 그래야 할 이유가 있을까? 어디서든 사용된다면 상관없지 않은가
project.tsx에서 project item에 대해 정의했다. 하지만 velog에서처럼 RWA는 component 아래에 ProjectItem.tsx를 생성하는 방향으로 작업했다. 어디서 사용될지 모르기 때문일까?
컴포넌트에 대한 test도 없다. provider는 말할 것도 없고. test를 어디에 적용할 것인가도 중요하게 생각해 봐야 할까? 너무 간단한 코드는 테스트할 필요가 없을 것이다.
컴포넌트와 컨테이너를 가르는 요소는 무엇일까? 난 지금껏 어떤 작은 파츠라고 연상되면 컴포넌트라고 여겼다. 하지만 UI의 크기와는 전혀 관계가 없어야 할 거 같다. RWA의 MainLayout 또한 컴포넌트이기 때문이다.
처음에는 state의 여부를 생각해 봤다. 컴포넌트들은 대부분 props로 데이터를 받아 출력하는 pure function으로 보였기 때문이다. 하지만 반드시 그런 것은 아니었다. RWA의 transaction과 관련된 컴포넌트들은 단순히 state를 가지는 것을 넘어서서 reducer에 event를 dispatch까지 하기 때문이다. 그렇다면 차이는 무엇일까?
"프로그램" 단계에서 관리하는, api와 관련된 데이터들, 그 "데이터"를 관리하는 것이 목적이라면 컨테이너라고 봐도 될 거 같다. 결국 데이터를 관리하는게 주목적이라면 컨테이너, 해당 데이터를 출력하는게 목적이라면 컴포넌트인것이다. 때문에 컴포넌트는 props로 데이터를 넘겨받는게 좋을 것이고, 컨테이너는 props가 아닌 직접적인 접근을 하는 것일 터다.
pakcage.json에 라이센스를 등록하고, 브라우저리스트와 엔진스로 환경을 명시해 줬다. 브라우저리스트에서 %
가 뭔지 궁금했는데 매우 재밌게도 전체 사용자의 비율이라고 한다. 이해한 대로면 크롬에서 > 5%
라면, 5% 이상의 유저가 사용하는 크롬 버전에 대해서 지원한다고 생각하면 된다. 매우 획기적인 접근 방법이라고 생각한다.
로직이 플랫폼에 종속적이어선 안 된다
하지만 어떻게?
프레임워크란 것 자체가 정해진 아키텍쳐가 있고 거기에 맞춰 설계해야만 한다. 그런데도 어떻게 종속적이지 않게 되는가? 로직을 어떻게 분리해야 하는가?
URL에 대한 고민을 이어가면서, 그런 생각이 들었다. 만약 내 프로그램이 React에서 Vue로 migrate를 하게 된다면 어떨까? 그래도 나의 이런 노력(수정사항에 대해 유동적으로 대처할 수 있게끔)이 의미가 있을까? 프로그램이란 건 각자 아키텍쳐를 가진다. 기본 설계가 있으며 그 설계에 따라 만들어진다. 아무리 변화에 유동적으로 프로그램을 구성한다고 하더라도, 그 기반이 송두리째 바뀐다면 코드 또한 통째로 바꿔야만 한다. 물론 그러한 노력이 그 과정을 최소한으로 하기 위한 거란 건 안다.
결국 설계에도 단계란 것이 있다. 기저에 가까운 설계가 변경될수록 소스코드의 변경 폭이 커질 것이다. 그렇다면 URL은 어떨까? Web 프로그램이라는 환경에서 URL은 어쩌면 가장 기본적인 설계가 아닐까? 그렇다면 URL의 변경은 거의 일어나지 않고 일어난다면 플랫폼을 바꾸는 것만큼이나 큰 변화인게 아닐까? 요약하자면, 결국 URL을 바꾸게 된다면 프로그램적인 측면에서 대규모의 변경이 일어나야만 하므로 내가 이렇게 URL을 하나의 변수로 관리하고자 하는 노력이 무의미한 건 아닐까?
그저 리터럴이라면 일은 간단해진다. docuemtns
에서 docuemtns + ':id'
로 이동하기만 하면 되는 것이다. 하지만 난 생각하는 거지, 과연 미래에도 document tab이 documtens/:id
로 구성되어 있을지...
리엑트 컴포넌트란건 뭘까. 정해진 입력에 따라 정해진 출력을 주는 '함수'다. 그렇다면 입력의 형태가 바뀔 경우 그 내용이 달라져야 하는 것은 자명하다. URL에 따라 렌더링 되는 컴포넌트가 다르단 것은 URL이 바뀜에 따라 컴포넌트도 반드시 변경되어야 함을, 아니 좀 더 정확히 말하자면 '재설계'되어야 함을 의미하는가? 그렇지 않다.
URL의 params에서 특정 입력을 축출하도록 컴포넌트를 설계할 수 있다. 이후 URL이 변경됨으로써 필요한 데이터를 더 이상 URL에서 가져올 수 없을지도 모른다. 그렇다 하더라도 컴포넌트를 재설계할 필요는 없지 않나. 필요한 데이터를 가져올 수 있는 다른 방법을 통해서 이제까지와 동일한 입력을 가져오도록만 해주면 된다.
분명 컨트롤할 수 없는 종속성도 있을 것이다. 차마 거기까지 생각하기 어려운, 그래서 때론 무시하고 작성해야 할 상황이 있을지도 모른다. 하지만 현재 날 고민하게 하는 URL을 구조만은 충분히 생각해 볼 만하지 않겠는가. 지금 생각하는 방법으로 router 구조가 굉장히 깊어지고 쓸데없이 복잡해질 여지가 강하다. 하지만 그렇다 한들 어떤가? 더 안정성 있는 구조를 선호하는 것은 나의 기호이며 굳이 부정해야 할 일은 아니다. 또한 완성된 모습을 지금으로서 정확하게 유추한다고 할 수 있는가? 난 내가 하고 싶은 대로 할 뿐이다
매치되는 루트가 없으면 기본 페이지로 리다이렉트 시키도록 한 적이 있다. 하지만 그건 좋은 방법이 아니다. 과거 있던 페이지가 삭제될 경우 사용자는 강제로 메인으로 리다이렉트 당할 것을 바라지 않을 것이다. 얼마나 당황스럽겠는가? 그보다는 현재 페이지가 삭제되었다는 것을 알려주는 것이 더 바람직할 것이다
처음 구상했던 프로그램은 다른 것이었다. 해당 프로그램도 상당한 시간 고민했던 것인데 기억은 나지 않는다. 다만 뭐가 되었든 개발 편의를 향상하고자 했던 것은 확실하다. 하나 기억나는 것은 웹 프로그램이지만 탭을 사용하도록 고집부렸단 것이다. 얼마 전까지 삼단 레이아웃을 구상하며 리스트와 컨텐츠를 한 화면 안에 구성하고자 하는 노력과 비슷하다. 그때나 지금이나 나는 하나에 집중하기보다는 여러 작업을 동시에 진행하는 편의를 갈망했던 것 같다.
지금의 프로그램은 노마드 코더의 트위터 클론 코딩을 진행하며 시작됐다. 구상하던 프로그램에 실시간 채팅 기능을 추가하고자 했던 것이다. 문서만을 표시하기보다는 gitter나 slack과 같은 프로그램의 역할도 수행하길 원했던 것이다. slack의 경우 github의 webhook을 이용해서 알림을 받는 등의 기능이 있지만, 난 더 나아가 웹브라우저상에서 코드 에디팅과 코드 share를 원활하게 할 수 있기를 원했다.
초창기 내 프로그램이 갖춰야 할 능력은 굉장히 방대했다. 각각의 프로젝트 안에서는 여러 기능이 지원됐는데, 그중 하나의 여러 채팅방을 통한 커뮤니티의 활성화다. 프로젝트 전체 채팅이 있으며, 각각의 opinion, task마다 혹은 그와 관련 없이도 채팅방을 자유롭게 생성하며 주제에 맞게 정보가 교류되길 원했다. 또한 해당 채팅에서는 단순한 링크 그 이상으로 코드를 직접적으로 리딩하고 에디팅하는 기능이 존재해야 했다.
그리고 문서화 기능이 있어야 했다. 아무 깃헙 리포지에 들어가서 코드를 보면, 한 파일당 3백 줄이 넘는 코드를 확인할 수 있다. 분명 주석이 달려있고 함수와 변수 네이밍에 신경 쓴 티가 나지만 코드를 이해하는 것은 쉽지 않다. 나는 그 가장 큰 이유를 모듈로 생각했다. 어쩌면 너무 추상화를 받아들이지 못해서였을까? 코드를 읽다 보면 알 수 없는 모듈(함수, 변수)가 등장했고 해당 내용을 확인하기 위해 여러 파일을 옮겨 다녀야 했으며 그렇게 해도 그 기저를 파악하는 것은 불가능하게 느껴졌다. 그렇다면 각각의 파일에 대한 설명과 그 파일 안 각각의 일급 객체들에 대한 설명이 제공된다면 좋겠다고 생각했을 뿐이다.
이 기능을 보면 JSDOC이나 TSDOC과 같은 기능을 떠올릴 수 있다. 하지만 내가 생각했던 것은 거기서 조금 더 나아간다. 특정한 형식의 주석만을 파악하는 것이 아니라, 코드 자체를 분석해서 함수의 존재를 알아채고, 해당 함수가 어떤 파일의 몇 번째 줄에 위치했으며, 해당 함수 내에 어떤 객체가 정의되며, 해당 함수를 정의하기 위해 어떤 외부 모듈이 사용되었는지 정리해 주길 바랐다. 어떤 의미 있는 문장을 활용한 정보는 당연히 주석을 통해 전해져야 하겠지만 그 이전에 일종의 그물망처럼 모듈의 의존 관계를 시각화했으면 한다.
하지만 이 기능을 구현하는 것은 너무 멀게만 느껴진다. 우선 컴파일러에 대한 지식이 너무 부족하다. 단순 구현에서 벗어나서 어떻게 최적화하는지를 고민해야 한다. 또한 언어의 문법이 갱신됨에 따라 나의 컴파일러도 해당 문법을 받아들여야만 할 것이다. 때문에 나는 tsc API를 사용하는 방법을 알아봤다. 하지만 이해하고 활용하기엔 너무 난해했다. 그래서 TSDOC을 먼저 적용하자고 생각했지만, 이 또한 복잡해서 어떻게 해야 할지 알 수 없었다.
새로운 언어를 만들어 보는 것처럼 Document Generator를 만드는 것은 내게 가장 큰 도전과제일 것이다.
결국 내게 남아있는 구현해 봄 직한 과제는 깃헙의 이슈란과 PR란이다. 즉, 이미 존재하는 프로그램인 셈이다. 이미 여기서 기운이 꺾인다. 하지만 분명한 차이점도 존재한다. 즉, 아직 도전 가치가 차고 넘친다.
기술 스택을 고민할 때, shopnote라는 프로그램을 통해 serverless 플랫폼을 소개하는 블로그 포스팅을 볼 수 있었다. 정확하게는 JAMStack에 대한 내용이었다. 당장 서버비를 지불할 생각이 없었기에 가장 완벽한 방법으로 보였다. 우선 TS를 사용하자고 생각했다. 다만 해본 프론트라곤 리엑트뿐인데 리엑트를 정말 하기 싫었다.
리엑트 코드는 굉장히 두서없어 보인다. 비슷한 로직은 비슷한 장소에 정제되어 있어야 한다고 생각하지만, 리엑트 코드를 작성하다 보면 그러기가 너무 어렵다. 어디에 위치해도 괜찮기 때문에 정말 쓰레기 코드가 탄생하기 좋은 거 같다. 또한 CRA가 너무 편리하다. 너무 많은 부분을 스스로 해내기 때문에 내가 손댈 부분이 없다. 그렇다고 아주 밑바닥부터 시작하기엔 또 막막했다. 기분도 상했겠다 새로운 시도도 해보고 싶겠다 선택한 것이 Elm이었다.
함수형 프론트 프레임웤이라는 타이틀 뒤에서 Elm은 굉장히 구미가 당겼다. 당시 purescript와 같은 다른 언어도 고민했지만, 더 유명하단 이유로 Elm을 선택했다. Elm이 JS 프레임웤이 아니라 하나의 새로운 언어라는 사실을 깨닫고 당황했지만 오기로 밀어붙였다. 하지만 사용자가 적어 생태계가 활성화되지 않았던 Elm은 나를 너무 힘들게 했다. graphql을 사용하기 위해서 필요한 Decoder를 일일이 생성하는 것은 비효율적이며 안정되지 못하다. 때문에 generator를 사용해야 하는데 만들어진 패키지 중에서 내게 적합한 것이 없었다. 뭔가 다운받으려 npm을 둘러보면 주간 다운로드 수가 나를 주눅 들게 했다. 어느새 Elm을 그만둘까 고민하다 마음을 다잡기를 반복하고 있었다. 그렇게 결국 Elm을 놓아 보냈다.
Elm은 내게 좋은 기억으로 남았고 난 여전히 호감을 가지고 있다. 언젠가 커뮤니티가 활성화된다면 난 Elm으로 돌아갈 의향으로 가득하다. 함수형을 처음 도입하며 프로그래밍에 대한 깨달음도 얻을 수 있었다. map 함수가 근본적으로 하는 일은 'mapping'이라는 것을 알 수 있었다. 이전까진 일종의 stream으로 여기고 있었는데 말이다. 또한 객체란 '상태'를 가져야 한다는 사실도 깨달았다. '상태' 없이 프로그램을 구성하는 것이 얼마나 힘든지 깨달을 수 있었다. 강제로 MVC 패턴을 사용하면서 모델이란 무엇인가 고민하고 조금 더 깊이 있는 이해를 할 수 있게 됐다. 리엑트와 리덕스의 관계를 모델과 컨트롤러, 뷰의 관계에서 찾아볼 수 있었다. curring이 왜 사용되는지도 알 수 있었으며, JS가 파이프 오퍼레이터를 지원하지 않음에 개탄하게 됐다.
함수형을 하게 되면서, 사실 Elm의 특징이 큰 영향을 줬다곤 하지만, Unit Test란 정말로 효용이 있는 것인가란 생각하기도 했다. 실제로 Elm은 Unit test보다는 Fuzz test를 지향하며, 커뮤니티 글 중에는 Unit test가 정말로 필요한지를 묻는 글들도 있었다. 해당 thread는 흥미롭게도 type driven development라는 단어가 등장하는데, 어쩌면 내가 TDD에 대한 환상을 가지고 있었는지도 모르겠다는 생각이 들었다. 애초에 테스트란 무엇인가... 언젠가 봤던 어떤 테스트 코드를 생각하면 어떤 컴포넌트가 어떤 class를 가지고 있는지만 주구장창 확인하던데 유용한 거 같기도 하다. case가 손에 꼽으며 case에 따른 특징들을 확실히 하고 싶을 수도 있다. 결국 어떤 test가 필요하며 어떻게 작성할지에 대한 경험치는 여전히 쌓지 못했다.
난 분명히 아이디어를 가지고 있었고 새로운 기능에 대한 열망도 있지만 아주 기초적인 부분도 제작하지 못하고 있다. 이유는 내가 모른다는 점인데 아직까지도 나는 공부해야만 한다.
netlify라고 하기엔 애초에 serverless가 처음이었고, Elm에 이어 TS도 새로운 것이었다. graphql 자체도 처음이었고, apollo 또한 도입해 본 적이 없다. 심지어 graphql 같은 경우에는 프로그램의 규모를 감안해서 apollo 없이 순수 graphql만으로 구현하고자 했었다. node만으로 웹 서버를 제작하는 것보다 express로 하는 것이 훨씬 쉬운 거랑 같은 맥락일까? apollo를 진작 도입해야 했을지도 모르겠다.
lint-staged와 husky, git hook의 존재를 처음 알고, micromatch를 이해하지 못해서 lintstagedrc를 고치기만 며칠을 한지 모르겠다. 아직도 github action은 설정하지 않았기 때문에, CI/CD 측면으로 공부해야 할 일이 많다.
netlify cli를 통해서 개발 서버를 여는 방법도, 디버깅하는 방법도 모르겠다. 우선 웹팩만 해도 플러그인과 로더의 종류가 너무 많다. 번들의 크기가 커서 줄여야 하는데 방법을 모르겠다. 다만 매우 긍정적으로 평가하는 부분은, 이제까지 수치와 최적화에 대해 전혀 체감이 없었다. 늘 "중요한데 왜 난 느끼지 못할까?"란 자책만 남던 부분에서 "이렇겐 안 된다. 어떻게든 줄여야 한다!"라는 인식의 변환이 돌아보니 감사할 일이다.
유저 보안에 대해서도 잘 모르겠다. 다른 사람들은 github oauth app을 만들 때, access token을 어떻게 보관하고 사용하는 걸까? 우선 나름대로 생각한 부분은 있지만...
스타일링도 나를 힘들게 한다. material-ui를 도입하며 다 해결될 거라고 믿었지만, 결국 기본 css를 사용해야만 한다. 가능하면 간단하게 하려고 해도 쉽지만은 않다는 게 서러울 뿐이다. 기존의 '의미 있는 class name'을 통해서 "f12와 개발자 도구로 내 코드를 살펴봤을 때, 가독성이 좋았으면 좋겠다!"란 생각도 버려졌다. 개발 측면에서 css를 컴포넌트와 같은 파일에서 관리한다는 이점이 그 모든 것을 뛰어넘는다고 생각한다.
material-ui의 createStyle을 사용한 hook 적용 방식에 불만이 있었다. "왜 tag의 style이 파일 상단에 위치해서 한눈에 들어오지 않는가?"란 짜증이 강했다. 하지만 그렇다면, 왜 다른 파일에 둬야 하는가? 결국 styled-component와 같은 inline styling이 옳았을지도 모른다.
graphql을 적용하는 것도 처음이었다. gql 자체는 어렵지 않았지만, 나를 힘들게 했던 부분은 서로 다른 언어로 작성된 코드의 동기화였다. 내가 gql schema를 작성했을 때, Elm이든 TS든 그에 맞춰서 type이 정의되길 바랐다. 일일이 수동으로 하는 것도 가능하지만, 그럴 경우 코드의 변경 사항이 적용되지 않을 테니까. 당연하지만 이 프로그램을 직접 개발할 생각은 없었다. 언젠가 하게 된다면 재밌겠다곤 생각하지만 당장 나에게 그 정도의 여유가 있을 순 없다.
처음 TS로 Server의 Resolver을 만들기 위해 Type-graphql을 도입했다. 이전부터 써보고 싶었던 데코레이터 패턴을 활용할 절호의 기회라고 생각했다. 하지만 eslint는 계속해서 에러를 뱉어냈고, 당시 interporation을 위해 __typename
을 요청했을 때 이상한 값이 반환됐다. stackoverflow에 질문했고, 당시 어떤 옵션에 대한 답글이 달렸지만 이미 지친 나는 gql-codegen을 도입한 뒤였다.
Elm을 관두게 된 가장 큰 이유도 gql 때문이었다. gql 공식 사이트에서 소개하고 있는 Elm-graphql은 서버와 클라이언트가 분리되어 있음을 가정했다. 하지만 난 gql schema를 가지고 있었기 때문에, 그렇게 할 필요가 없었으며, 오히려 서버와 클라이언트를 한 번에 업로드하기 때문에 부적합한 상황이었다. 다른 패키지를 찾아봤지만, 너무 낮은 다운로드 숫자와 제공하는 기능의 부실로 좌절할 수밖에 없었다.
gql과 관련되어서 DB에 대해서도 일이 있었다. 처음 Serverless 플랫폼으로 firebase를 고려했다. GCP는 충분히 큰 인프라를 가지고 있기에 문제가 없다고 생각했다. 하지만 firebase가 무료로는 cloud-function을 지원하지 않는단 사실을 알게 됐고, netlify를 사용하기로 결정하게 되면서 Fauna를 사용하리라 마음먹었다. Fauna를 사용하려 했던 가장 큰 이유는 NoSQL이면서 SQL처럼 사용할 수 있단 점과, 쿼리 언어로 GQL을 사용함으로써 러닝 커브를 줄일 수 있단 계산이었다.
하지만 GQL만으로는 제대로 된 DB를 사용하기 어렵다고 판단하게 된다. 때문에 Mongo를 도입하려 하지만, Mong Realm의 존재를 알게 되고 고민한다. Serverless 자체가 플랫폼에 의존적인데 netlify를 사용하면서 Fauna를 사용하지 않는 게 옳은 일일까? netlify는 Fauna와의 연계를 위해 add-on도 제공하고 있다. 그렇다면 결국 FQL을 새롭게 공부하며 Fauna를 사용하는게 맞겠다고 판단하게 되었다.
webpack dev server를 통해서 client dev server를 구성하는 데 성공했다. netlify cli와의 연계도 훌륭하다. webpack 공식 문서의 build performance란을 참고했는 데, 확실히 특정 설정을 끄는게 많은 도움이 되는 것 같다. 추측하기로 특히 ts를 사용하면서 많이 느려지는 것으로 보인다. ts loader의 transpile only가 영향을 많이 주지 않았나 생각한다. forktsplugin을 제거하니 reload 성능이 올라갔기 때문이다.
걱정하던 material-ui를 external하지 않아도 성능엔 크게 문제가 없는 것으로 보인다.
문제는 netfliy functison를 테스트하는 데 있다. netlfiy-lambda를 설치해서 사용해 봤지만 크게 2가지 문제가 있다. 첫째론, serve가 legecy가 되면서 netlify dev가 serve가 아닌 build를 인식해 작동한다. build로 작동할 것 같으면 무슨 의미가 있단 말인가? 다만 이 문제는 netlify cli가 아닌 netlfly-lambda만을 사용한다면 해결될 문제다. 아마 netlfy cli에서 추후 기능이 추가될 것으로 기대되긴 한다.
둘째로 webpack.config.ts에서 import문을 사용하니 에러가 발생한다. eslint 설정상 import를 require 대신 사용해야 하는 입장에서 import error는 당황스럽다. graphql-codegen 때문에 package.json의 type 항목을 module로 설정하는 것도 불가능하다. 관련 검색을 해 봤지만, 애초에 nentlify-lambda를 사용하는 인구가 너무 적은 것으로 보인다.
사실, webpack 최적화 측면으로도 그렇고, netlify-lambda의 설정상으로도 babel을 도입하면 괜찮지 않을까 하는 희망이 있다. 다만 이 경우 babel에 관해서 공부해야 하며, babel을 적용한다고 해서 반드시 성공한다는 보장이 없다. 어떡해야 할지 막막한 상황이다.
babel을 이제와서 도입하기보다는 netlify cli만을 사용하며 prestart로 fucntions만 빌드하는게 어떻나 하는 생각도 있다. 어쩌면 develop과 prodcution에 따라서 handler를 return할지, server를 열지를 결정하게 될 수도 있다.
현재 development와 production의 구분은 webpack의 argv로 하고 있다. 하지만 어쩌면 .evn를 사용해야 할지도 모르겠다. 왜냐하면 apollo를 설정할 때, development와 production의 차이가 있어서 이 부분을 유의미하게 활용하고 싶다.
netlify-lambda를 다시 사용하는 건에 대해서 오래 고민했다. 아예 netlify-cli를 사용하지 말고 netlify-labmda의 serve를 이용해서 dev server를 잘 활용한다면 나쁠 게 뭐가 있나. 하지만 아무리 생각해도 나빴다.
netlify team은 netlify-cli를 지속적으로 업데이트하겠지만 netlify-lambda는 그럴 것 같지 않다. 내 생각엔, netlify-cli에 추후 netlify-fucntions를 build 하는 기능이 추가될 것이다.
netlify-labmda의 serve를 직접 사용해 보진 않았지만, script의 코드 중에서 serve가 build를 참조하는 것으로 보아 in-memory를 사용해 server를 여는게 아닌 watch 모드로 build를 진행한 후 그에 따라 서버를 re-load 하는 것으로 보인다. 그렇다면 그냥 내가 webpack watch모드를 사용하지 않을 이유가 없다.
babel을 사용해야 하는데, babel을 적용하는 게 그렇게 어렵진 않겠지만, 상당히 귀찮을 것이다. 또한, 이미 ts-loader를 사용해서 관련 작업을 끝내났는데 다시 진행하면서 또 어떤 산길로 빠질지 알 수 없다.
그렇다고 webpack.config에서 require를 사용하기에는 eslint 설정뿐이 아니라 여러모로 내키지 않는다.
위 사실을 고민하다 보니, 예상처럼 netlify-labmda의 serve가 watch모드의 build를 통해 행해진다면 내가 직접 webpack으로 하자는 생각이 들어서 시도해 봤다. 이 과정에서 webpack.config 파일을 2개로 나눠야만 하게 되었는데, 이 파일들의 위치를 어떻게 할지도 고민이었다. client를 기본으로 두고 functiosn의 config만 functions 폴더 내부에 위치시킬까도 생각해 봤지만, functions 내부에는 오직 source만을 두고 싶었기에 참았다. 만약 webpackconfig가 functions 내부에 위치하게 될 경우 eslintrc과 같은 설정들도 분리하고픈 욕구를 참지 못했을 것이다. 그런식이라면 결국 새로운 패키지로 분리하는게 적합한 상황까지 왔겠다고 생각한다.
결국, webpack이라는 하위 폴더를 생성하고 config 파일을 모았는데 이런 결정할 한 가장 큰 이유로는 vscode의 icon이 있다. webpack-client.config.ts와 같은 형식을 사용하면 아이콘이 webpack 으로 변하지 않는다...! 이건 매우 있어서는 안 될 일이다.
설정하는 과정에서 eslint plugin에 대해 고민하게 됐는데, 왜냐면, babel-loader를 사용하게 될 경우 별도의 type-chek 과정이 필요하고, 보통 사람들은 tsc —noEmit
으로 type-checking을 진행한다. (이게 또한 ts-loader를 포기하기 싫었던 이유 중 하나다) 결국 type-checking이 실패한다면 build가 fail 하게 된다. 마찬가지로 eslint로 linting에 실패한다면 build를 fail 하도록 만들고 싶어졌다. 생각 외로 좋은 옵션 중 하나가 waring에도 fail 하도록 설정할 수 있었는데, 이건 매우 좋다! 내 취향이다!
결과적으로 dev-server를 오픈하는 데 성공했다. 남은 고민이 몇 가지 있는데 정리해 두자
ui styling을 대략 넘겨두고 apollo 기본 설정을 시작했다. client를 생성하고 server를 재확인하는 과정을 거친다. 우선 client 생성을 처음에는 app.tsx에서 수행했는데, material-ui의 theme을 별도의 파일로 분리한 것처럼 이것도 분리하는게 옳다는 생각이 들었다. 여러모로 고민했지만 파일 이름을 통해 전해지는 정보가 중요한 만큼 결국 apollo client는 별도의 모듈로 분리되었다.
이후 서버와 클라이언트 양쪽의 기본 기본 문서를 훑었는데 graphql document를 .graphql로 작성하는 것이 성능 면에서 낫다는 언급이 있었다. 때문에 다시 ts파일을 .graphql로 바꾸기 시작했는데 webpack에서의 로더는 문제가 없었지만, tsc가 인식하지 못하는 상황이 발생했다. (내가 이전 포기했던 이유를 잊고 있었다) 이런저런 검색 결과 결국 별도의 .d.ts 파일을 생성해야 함을 알게 됐다.
.d.ts파일을 생성하는 방법도 잘 모르는 데다가 (물론 구글링 결과를 복붙하면 되기는 하겠지만...) 내키지도 않아서 이리저리 방황하던 중, graphql-codegen에서 .d.ts를 생성해 주는 플러그인이 있음을 알게 되었다. 가뭄에 단비를 만난 듯이 바로 확인해 본 결과는 놀라웠다. 굳이 .d.ts파일을 생성할 필요가 없이 useQueryName형식의 모듈을 봔한, 즉시 hook으로 사용 가능하도록 하는 ts파일을 생성해주는 기능이 있었다. 다만 아쉬운 점은 shcema에 대해선 그런 기능이 없어 보였다는 것이다. 이전의 .d.ts의 문서를 읽어봐도 예제가 모두 schema가 아닌 document였던 것으로 보아 schema는 제대로 실행되지 않을 것만 같았다.
아폴로 서버 문서를 보니, 이곳에는 성능에 대해 .graphql 파일에 대한 언급이 없었다. 서버는 번들링 할 이유가 굳이 없는 것처럼, 어쩌면 이쪽은 성능에 대해 이슈가 없지 않을까 하는 생각이 들었다. 지금 생각해 보면 serverless를 사용하는 만큼 cold start 단계에서 분명 이슈가 있을 것으로 보이긴 하나 일단은 괜찮다고 생각하고 클라이언트 쪽에만 적용하기로 했다.
lint-staged에서는 linting 여부를 확인하고 있었다. 하지만 type-check도 진행되어야 하지 않을까? 알아보니 tsc —noEmit으로 type-check를 진행하고 있었다. 처음에는 이 부분을 lint-staged에 포함했지만, "수정파일 만 확인한다고 해서 프로젝트에서 type-checking이 제대로 이뤄지지 않는다"는 사실을 깨닫고 전체 프로젝트에 실행시키도록 수정했다. 성능상에 이슈를 걱정했지만 크게 느리진 않을 거라 애써 무시해 본다.
다만 test 코드를 작성하게 됐을 때 처음에는 컴파일 오류가 발생하게 될 것이다. 이 경우 커밋 이전에 컴파일 오류를 해결할 스켈레톤 코드를 작성해야만 할 것인데, 이게 매우 마음에 들지 않았다. 이에 대해 친구에게 고민을 토로했었는데, 친구 말로는 커밋할 시 반드시 컴파일은 성공해야만 한다고 한다. 이는 TDD의 경우도 마찬가지이며 test code를 작성한 후 컴파일까진 되게 한 뒤 커밋하는게 맞다고 한다. 난 TDD의 경험이 없으므로 (다행으로 생각하며) 기쁜 마음으로 조언을 받아들였고, 그 결과 huskyrc에 typecheck 과정을 추가하게 된다.
type check 과정에서 tsc가 지속적으로 node_modules를 컴파일하며 오류가 발생하는데, 그 이유가 @types 때문으로 보인다. 때문에 tsconfig의 types를 []
로 수정했다. 이 경우 type이 제대로 불러와지지 않을까 걱정했는데, global 하지 않게끔 하는 것이라 오히려 진작 추가하지 않은 것이 아쉬웠다. 여전히 type system은 제대로 동작하며 컴파일은 성공적.... 이어야 했으나 webpack.config.ts에서 문제가 발생했다.
webpack-dev-server의 type이 컴파일 되지 않았던 것이다. 이리저리 수를 써 보았지만 제대로 실행되지 않았고 결국 지친 난 이 시점에 우선 자게 된다.
일어난 후 이런저런 고민 끝에 config 파일을 js로 작성하며 project에서 제외하기로 결정했다. 생각해 보면 애초에 이상한 것이 project src에 config를 포함하는 것은 부적절하다. 때무에 내 프로젝트 소스는 오직 src와 functions 폴더만 포함하게 되었으며 각종 rc와 json과 config.js 파일은 오직 설정 파일로서 남아 lint와 type-check에서 제외되었다.
apollo hook에서 react component가 unmount 된 이후 state를 변경하게 되어 경고가 출력됨을 확인했다. hook이 비동기적으로 state를 변경시키는게 원이었는데 경고문은 useEffect를 사용할 것을 조언하고 있었다. 검색을 통해 lazy hook을 사용하긴 했지만, useEffect의 clean up function을 디자인할 수 없었다. 일단 오류는 발생하지 않는데, 추측하건대, 데이터를 불러오기 전에 unmount되게 되면 자동으로 abort 하는 것으로 보인다. (정말 그런 걸까? 그래야만 하는데...) 근거로는 data를 load 한 후 컴포넌트를 unmount하게 되면 server측에서 websocket이 hang up되었다는 메시지와 함께 에러가 출력되기 때문이다.
결과적으로 clean up 함수는 찾을 수 없었고, lazy hook이 자동으로 abort하기를 바라고 있다. 이후 자세한 정보를 알아볼 필요가 있다.
netlify의 경우 NODE_ENV가 production이면 dev dependancies를 install하지 않는다. 매우 당황스러운데, 왜냐하면 @types와 같은 dependencies 또한 dev가 아니게 되어 버리기 때문이다. 때문에 현재는 prebuild로 install을 등록해 둔 상황이다.
빌드 과정에서 사용되는 것들을 dev에서 제외해야 할까? 하지만 난 빌드 과정에서 다양한 문제를 검출할 수 있기를 바라고, 때문에 테스트나 린트, 타입체크를 실행할 것이다. 이 모든 것들이 dev에 들어가야만 한다고 생각한다.
우선 처음 생각나는 것은 env, 특히 NODE_ENV에 관한 것이다. 관련 정보를 찾아보면서 흥미로운 사실을 알게 되었는데, 현재는 React와 같은 frontend lib를 npm으로 관리하는 것이 정석이다. 이 모든 파일을 webpack 등을 이용해서 빌드하게 되는 것이다. 하지만 옛날에는 그렇지 않았다고 한다.
여기서 과거 가장 인기 있었던 express에서 env를 결정하던 방식이 원래는 EXPRESS_ENV 환경변수를 사용하다가 NODE_ENV를 사용했다고 한다. 이에 맞춰서 많은 프로그램이 NODE_ENV를 통해서 mode를 결정하는 관행이 생겼다고 한다. 이 때문에 webpack에서도 mode에 따라 NODE_ENV를 설정한다.
env와 관련해서 첫 헛질은 dotenv와 webpack-detenv를 설정한 것이다. netlify dev에서는 자동으로 local root의 .env를 참조하여 env_var을 덮어쓴다. 따라서 골치 아프게 webpack plugin을 다시 설치할 필요가 없었다.
두 번째로는, apollo client에서 debug tool을 설정하며 깨달은 점인데, 신기하게도 forntend lib에서도 NODE_ENV로 mode를 결정한다. 하지만 front에서는 proccess.env에 접근할 수 없다! 그래서 다시 webpack-dotenv를 적용해야 하나 고심했지만, 결과적으로 webpack은 mode를 설정하면 그에 따라서 proccess.env.NODE_ENV를 자동으로 설정하고 있었다. 또한 Defineplugin의 특성상 NODE_ENV값을 변수에 저장하지 않고 리터럴 그대로 사용함으로써 컴파일 영역에서 쓸데없는 코드를 줄일 수 있다고 한다. (갓갓!)
덕분에 NODE_ENV를 별도로 설정할 필요가 없고 netfliy가 build시에 모든 dependancy(dev 포함)을 설치할 것이기 때문에 기존의 devdepancacy를 유지할 수 있게 되었다.
함수 타입 anotation에 대해 처음에는 기존 방법을 고수했다. 하지만 react에서 props 타입이 arrow형태를 띠고 있는 것을 확인하고 그 형태로 일괄 변경했다. 이 과정에서 eslint/react rule 중 props validate를 꺼야 했다.
하지만 문제는 generic에서 발생했다. arrow fucn의 type annotation으론 generic func를 표현할 수 없었다. 깊은 고민 끝에 결국 원래의 방법으로 일괄 변경했다.
바벨을 적용할 경우 빌드 속도가 빨라진다 (material ui). 하지만 아직 바벨을 적용하기엔 무리다 (공부량이 너무 많아질 것이다!) 그렇다고 기존의 세부적인 import로 돌아가기엔 컨벤션이 맘에 들지 않는다. 가능한 비슷한 import를 그룹으로 묶어서 관리하는게 훨씬 도움이 된다고 생각한다.
++ 관련해서 import 시 as를 통한 re-name의 룰도 정립해 두면 좋을 거 같다
어쨌든, 바벨 또한 env를 명시하는 데 도움이 된다. 현재 나는 오직 chrome에서만 작동시킬 것을 상정하고 프로그램을 작성하고 있기 때문이다. 다만 이 경우 node의 버전을 명시해야 하는데(서버 쪽), .nvmrc등을 설정하는 방법을 알아보기 싫다...
ci cd pipeline과 관련해서 그냥 husky로 로컬에서 대부분을 실행하기로 마음먹었다. test coverage 때문에 원격을 사용해야 할지도 모르겠으나, 아직 test를 도입하지 않았기 때문에 큰 문제가 없는 상황이다.
파일 구조를 살짝 바꿨다. eslintrc과 webpack config를 client와 lambda를 서로 구분했다.
순환 참조 문제가 발생했다! 처음 시작은 URL의 match params인데, 결국 상위 라우터의 정보가 있어야지만 하위 라우터에서 완벽하게 라우팅을 관리할 수 있다. 이 때문에 서로 순환 참조가 발생하는 것을 확인했고, 그 과정에서 새로운 컨벤션을 도입했다. 폴더 내의 indeex 파일에 모든 export를 담당하고 다른 모든 파일은 "반드시" index로부터 import 한다.
react, apollo dev tool을 연결했다. debugger는 당장 필요성이 느껴지지 않으니 pass!