개발 일지를 써볼까~
오늘은 커밋을 6개 올렸다.
Handle Error Properly
tryLogin
처리 중 발생하는 오류를 잘 처리하도록 수정하였다. 일단 tryLogin
이 뭔지 설명해야겠지...
아지트에서 "네이버로 로그인"을 클릭해서 네이버 쪽에서 로그인이 되면, 아지트에는 /login/naver/callback
으로 리다이렉트된다. 이 주소에 이렇게 네이버를 거쳐서 정상적으로 접근한다면 딱 한 번 접근권(access token)을 받을 수 있는 코드(authorization code)가 주소에 쿼리로 전달된다. 아지트의 클라이언트 코드에서는 Next.js 서버 액션으로 tryLogin(code)
를 호출한다. tryLogin
은 네이버 계정의 아이디를 알아내 그 아이디로 등록된 사용자가 있으면 로그인하고, 없으면 사용자 등록을 위한 코드를 만들어 리턴한다. 그렇게 동작하는 tryLogin
의 타입은 다음과 같다:
type tryLoginResult =
| Register({code: string, naverName: string})
| Login(loginResult)
let tryLogin: string => promise<tryLoginResult>
이때의 문제는 사용자가 일부러 또는 실수로 콜백 주소를 잘못된 코드로 접근했을 때 블로그가 뻗어버린다는 것이다. 그건 서버 내부에서 수정해야할 에러가 아니라 사용자의 잘못이고 예측가능하다. 따라서 다음과 수정한다:
type tryLoginSuccess =
| Register({code: string, naverName: string})
| Login(loginResult)
type tryLoginError =
| Unauthorized
let tryLogin: string => promise<result<tryLoginSuccess, tryLoginError>>
정확히는, 네이버에 접근권을 요청했을 때 응답 데이터를 까봤더니 접근권이 없거나, 접근권을 잘 받았는데 그 접근권으로 네이버 아이디를 요청했는데 이번엔 응답 데이터에 아이디가 들어있지 않을 때, 그런 때에 Unauthorized
로 리턴한다. 그렇게 리턴해도 프론트엔드에서는 에러메시지를 보여줄 뿐이지만... 아지트 자체가 뻗어버리는 것보다는 낫다.
이걸 고치면서 예외처리에 대한 철학이 섰다. 예상가능한 예외만 처리하자! 예를 들어 코끼리 SQL 서버가 응답을 안하는 일은 있어서는 안 되고, 그런 일이 일어난다고 해도 아지트에서 할 수 있는 일은 없다. 따라서 그건 굳이 result
써가면서 고생한 필요 없이 예외로 던져버리면 그만이다. 데이터베이스에 물어보는 함수는 이렇게 바뀐다:
let query = async (f, param) => {
try {
let result = await client->f(param)
await client->release
// 기존 코드: result->Ok
result
} catch {
| e => {
await client->release
// 기존 코드: e->Error
raise(e)
}
}
}
Ok
니 Err
니 해서 result를 만들지 않고 에러로 던져버린다. 야호! 사실 회원가입하거나 할 때 이름 중복 체크를 데이터베이스에 맡기기 때문에 예외를 까봐야할 때가 없지는 않지만, 그거야 말로 예외처리의 예외 상황이니 매번 오류를 무시하는 코드를 귀찮게 넣기보다는 예외를 까보는 코드를 귀찮게 쓰는 게 맞다.
Create Actions.resi
Actions.res
는 'use server'
로 시작하는, 서버 액션을 모아놓은 '서버 모듈'이다. Next.js의 파격적인 행보랄까, 한 모듈에서 내보내는(export) 모든 함수를 API 엔드포인트처럼 노출하겠다, 그런 이야기다. 덕분에 편해지긴 하지만 이것이 Rescript와 결합하여 좋지 않은 효과를 연출한다. Rescript 모듈은 기본적으로 모든 함수와 선언을 내보내도록 컴파일된다. 그 말은 예를 들어 사용자 정보만을 받아 바로 접근권을 만들어 리턴하는 login: userProfile => promise<loginResult>
같은 함수를 전역에서 정의할 수 없다는 뜻이다. 보안과 코드 재사용성 사이에서 유쾌하지 못한 줄타기를 해야한다. 그 지점에서 이제는 미뤄왔던 일을 해야할 때가 왔음을 알았다. 바로 문서화...! 문서화라고 하기도 뭣하지만 "돌아가게만" 하는 것 이상의 코드를 써야 한다는 점이 한발짝 나아가는 일이라고 볼 수 있다. 잔뜩 의미부여하고 있지만 지금까지도 돌아가게만 짜지는 않았었다. 정말로는 그냥 안 쓰던 파일 확장자를 하나 쓰게 된 것 뿐이다. 어쨌건 그런 일이 하나의 커밋으로 남았다.
Rescript의 모듈 구현 파일은 .res
확장자를 쓴다. 일반적으로는 이 파일만 있어도 되지만 코드의 내장을 다 드러내기 싫을 때는 .resi
파일에 공개할 함수와 값의 타입만 적을 수 있다. 이름하야 Rescript 모듈 인터페이스 파일이다. 서버 모듈이라는 특수한 경우, 클라이언트가 내장을 마음대로 주무르게 할 수 없기 때문에 이 인터페이스 파일은 필수라고 할 수 있다.
사실 이 커밋에서는 Utils.res라는 파일도 만들고 디렉토리도 2개 만들어서 파일들을 정리했다. 커밋이란 게 다 그렇지 않은가. Utils.res에는 result와 option 타입을 async/await으로 쉽게 다루기 위한 함수들을 정의해놓았다. Rescript는 확실히 만들다만 언어라는 느낌이 드는게, 표준 라이브러리가 다른 함수형 언어에 비해 빈약하다. 다른 사람들이 만든 거라도 있으면 좋겠지만 쓰는 사람이 없으니... 내가 만들어쓰는 수 밖에 없다.
잡다한 커밋들
- Add Logout: 로그아웃 기능을 추가했다. 별 일은 없었고 그냥 기존 함수를 그대로 옮겨왔다. 쿠키를 지우고, 재발급권(refresh token)을 데이터베이스에서 지우는 정도의 동작이다.
- Add Responsive Menus: 왼쪽에 있는 메뉴를 추가하고 스크롤할 때 헤더가 들어갔다 나왔다 할 수 있게 했다. 예전에 타입스크립트로 개고생해서 써놓은 게 있어서 그대로 옮겼다. CSS를 Tailwind로 바꾸는 것도 꽤나 일이었다. 이거 유지보수 되는 건가? 하는 생각이 들지만 유지보수 안 되는 건 CSS 파일에 모아놔도 마찬가지이지 않을까. 이거 쓰면서도 결국은 브라우저 계속 띄워놓고 작업했으니 말이다.
- Fix
main
X Overflow: 코드 너비에 따라 페이지가 양옆으로 마구 늘어나는 버그를 해결했다. 어떻게 해결했나를 알려면 CSS를 해독해야 하는데..., 되게 짧은 커밋이라 금방 해독했다. flex로 정렬된 자식 요소들은min-width: 0
을 먹여주지 않으면 flex가 관리를 포기한 금쪽이가 되어버린다.pre
에min-width: 0
먹였다가 잘 안 되었었다. 이건 기억하고 있는게 아니라 커밋 내역에 나와있다. 그냥 둘까 하다가 나중에 해독할 일이 있을 수도 있으니 추가 커밋으로 제거. 아 이런 때는 amend를 했어야 했나? 이렇게 밑바닥에 깔린 커밋에도 어멘드가 되던가... 어멘드 자체를 많이 안 해봐서 모르겠다. - Change Misc Styles: 그냥 이것저것 했다. 모바일에서 메뉴 열 때 스르륵 색깔 변하는 걸 구현해보려다 관둔 흔적이 있다. 이 부분은 나중에 또 건들테니까 그냥 둬야지...
이상 말많은 개발일지 1일차를 마친다. 이런 식으로 써서는 2일차는 쓸 엄두가 안 날 것 같다. 이번엔 레포를 깃허브에 올리긴 했는데 공개할지는 고민중이다. 결국은 공개할 것 같지만 지금은 다른 할 일이 기한이 애매해서 미뤄놓고 몰래 하는 느낌이라... 당분간은 숨김 상태다.
- 삼권분립의 현장. 각각 Next.js, Rescript, PgTyped 명령어다. 저장할 때마다 뭔가 다시 컴파일한다. 각자의 자리에서 최선을 다하길. 가끔 서로 말이 안 맞으면 말이 맞을 때까지 한놈씩 죽여본다. 사이좋게 지내렴^^