Joonmook 프로필 사진

@Joonmook

Blog

오래된 Express 블로그를 갈아엎지 않고 살린 이유

오래된 Express 블로그를 갈아엎지 않고 살린 이유 썸네일

이 블로그는 Express와 EJS, MySQL로 만들었다. 요즘 기준으로는 올드스쿨이라 몇 달 전에 Next.js로 갈아엎겠다고 글까지 썼다. 결론부터 말하면 안 갈아엎었다. 대신 며칠 동안 코드를 처음부터 끝까지 다시 읽었고, 그 과정에서 프레임워크가 문제가 아니었다는 걸 알게 됐다. 문제는 구멍이었다.

찾은 것들

부끄럽지만 그대로 적는다. 나중에 같은 실수를 하지 않으려면 적어두는 게 낫다.

  1. 글 작성, 수정, 이미지 업로드 라우트에 인증이 없었다. 로그인 페이지에는 잘 걸어놓고, 정작 POST 라우트에는 미들웨어를 안 붙였다. 누구든 폼만 만들면 내 블로그에 글을 쓸 수 있었다.
  2. 회원가입이 열려 있었다. 로그인 화면에는 "가입 불가"라고 써놨는데 URL로 직접 가면 됐다. 가입하면 글을 쓸 수 있다.
  3. 검색어를 그대로 innerHTML에 넣고 있었다. 최근 검색어 목록이 그렇게 만들어졌다. 실제로 DB에는 경로 탐색 문자열과 코드 조각이 검색어로 쌓여 있었다. 그중 하나는 예전 내 코드 버그가 만든 것이었다. 템플릿 문자열을 일반 문자열로 써서 코드 텍스트 자체가 링크가 됐고, 누를 때마다 그 텍스트가 검색어로 저장됐다.
  4. 에러가 나면 응답이 없었다. try/catch에서 console.error만 하고 끝. 브라우저는 영원히 기다린다.
  5. 사이트맵의 글 링크 14개가 전부 404였다. 1년 전에 생성기로 뽑은 정적 파일이었고, 그 사이 글 ID가 다 바뀌었다. 구글에 물어보니 색인된 페이지가 로그인 페이지 하나였다.

이 목록을 보고 나니 Next.js로 옮기는 게 무슨 의미가 있나 싶었다. 옮겨도 인증 미들웨어를 빼먹으면 똑같다.

고친 것들

순서가 중요했다. 보안부터, 그다음 응답, 그다음 검색 노출, 마지막에 속도.

// 인증을 multer 앞에 둔다. 로그인 안 한 요청은 파일이 디스크에 닿기 전에 끊긴다.
router.post('/write', verifyToken, uploadSingle('thumbnail'), blogController.writeBlog);
router.post('/modify', verifyToken, uploadSingle('thumbnail'), blogController.modifyBlog);
router.post('/upload', verifyToken, uploadSingle('image'), fileUpload);

글 저장은 트랜잭션으로 묶었다. 예전에는 태그와 이미지를 forEach 안에서 async로 던져놓고 기다리지 않았다. 절반만 저장되는 상태가 가능했다는 뜻이다. 전역 에러 핸들러를 달아서 어떤 경우에도 응답이 나가게 했고, 컨트롤러는 next(err)만 하도록 정리했다.

검색 노출은 코드보다 죽은 링크와의 싸움이었다. 정적 sitemap.xml을 지우고 DB에서 매번 생성하도록 바꿨다. robots.txt와 RSS도 없었길래 만들었다. 글마다 canonical, og:type=article, 발행일, JSON-LD를 넣었다. lang 속성이 en으로 되어 있던 것도 ko로 바꿨다. 이런 건 하나하나는 사소한데, 없으면 검색엔진 입장에서 이 사이트가 뭔지 알 방법이 없다.

속도는 측정하고 나서야 어디가 문제인지 알았다. 폰트 파일 4개가 합쳐서 3MB였다. Pretendard 전체 파일을 서빙하고 있었는데, 유니코드 범위별로 쪼개진 서브셋으로 바꾸니 수십 KB로 줄었다. 정적 파일에 캐시 헤더가 없어서 재방문에도 전부 다시 받고 있었고, 썸네일은 원본 그대로였다. 업로드 시점에 1600px 이하 WebP로 변환하도록 바꿨다.

배운 것 세 가지

  1. 보안은 기능이 아니라 순서다. 미들웨어를 어디에 두느냐가 곧 정책이다. 라우트 파일을 위에서 아래로 읽으면서 "이 요청이 여기 도달하려면 무엇을 통과했나"를 매번 물어야 한다.
  2. SEO는 죽은 링크와의 싸움이다. 메타 태그보다 먼저 볼 것은 내가 검색엔진에 알려준 URL이 살아 있는지다. 정적으로 뽑은 목록은 반드시 썩는다.
  3. 성능의 80%는 폰트와 이미지다. 코드 최적화는 그다음이다. 측정 없이 코드부터 손대면 시간만 쓴다.

그래서 갈아엎기는

나중에 한다. 지금은 문제가 뭔지 알고, 그 문제를 이 스택 안에서 다 고쳤다. 갈아엎고 싶은 마음은 대부분 "코드를 다 읽기 싫다"는 마음이었던 것 같다. 다 읽고 나니 갈아엎을 이유의 절반은 사라졌다. 남은 절반은 언젠가 진짜 필요할 때 하면 된다. 그때는 이번에 만든 테스트와 배포 스크립트가 그대로 따라올 거다.

Comments 0

No comments yet. Be the first!

← Back to Blog