Next.js App Router 삽질기

Next.js App Router 삽질기

개발 이야기
Junhyuk Lee

2023년 6월 28일에 서초의 오픈소스 소프트웨어 통합 지원센터에서 열린 리액트 코리아 밋업에서 발표한 자료입니다.

이날 3명 중 한명으로 발표를 하게 되었는데 후킹이 잘된다는 이유로 첫번째 발표를 하게 되었네요. 아이고 부담시러라 ㅋㅋ 내용은 최대한 딱딱하지 않고, 듣기 재미있게 하기 위해 반존대로 진행했는데, 혹시나 이게 막 불쾌하게 들리지 않으셨으면 합니다.

001.jpeg001.jpeg

안녕하세요, 방금 인트로 진행해주신 요한님의 부사수로 첫 회사에 입사를 해서, 개발에 입문하게 된 이준혁이라고 합니다. 주로 미리야라는 아이디로 아가리파이터 활동을 했는데 아저씨들 말고 요즘 분들은 아마 잘 모르실거에요. 사회화가 되면서 점점 전투력이 떨어지더라구요.

오늘은 제가 최근에 Next.js 13버전과 함께 출시된 앱 라우터를 사용하며 겪은 온갖 더러운 종류의 버그들과 해결 경험들을 공유해보려고 합니다. 일단 밑밥을 깔아보겠습니다.

002.jpeg002.jpeg

제가 개발을 해온지 햇수로 두자리가 조금 넘어가는데요, 나이가 있다 보니까 팀장도 하게 되고, 팀장이다 보니 외부에서 들어오는 이력서를 볼 일이 되게 많아졌어요. 하루에 50개 넘게 들어오는 이력서를 스크리닝을 해야되는데, 자연히 기계적으로 보게 됐지요.

(깐깐한 말투) 야 너 레포지토리에 타입스크립트 없어- 탈락. 야 너 레포지토리에 이틀전 투두리스트? 너 작년에 졸업했는데 6년차라며, 그짓말쟁이 탈락. 야 너 클래스 컴포넌트? 히익 너 고인물이구나? 탈락.

근데 이게 제 3자의 눈으로 보니까 제 깃허브가 더 끔찍한거에요.

003.jpeg003.jpeg

자 코드를 보자.

캬, 오토바인드, 캬, 클래스 컴포넌트, 캬 프롭타입스, 거의 18세기식 리액트 코드네요.

컴포넌트 디드마운트, 슈드 컴포넌트 업데이트, 컴포넌트 디드 업데이트, 렌더, 캬 프래그먼트 뒤에 멋지게 띄어쓰기까지! 여러분들 이거 알아요? 옛날에는 리액트 이렇게 짰습니다.

나중에 이직할 때 이직할 회사 면접관이 제 레포지토리를 보고 “하아…” 하고 한숨쉴게 뻔해 보였지요. 왜냐하면 제가 그랬으니까.. 그래서 결심했죠, 싹 지우자! 가자 넥스트의 세계로!

004.jpeg004.jpeg

야 여태 10년 넘게 남의 돈 받고 남의 서비스 만들어줬으면 열심히 한거다. 이제는 내 것을 좀 만들어보자- 하는 생각이 들더라구요.

우리 프론트엔드 개발자들이 실무를 하다 보면 항상 회사의 변덕을 맞딱뜨리게 됩니다. 시작할때는 분명히 Create React App으로 충분했거든요? 근데 회사에 갑자기 마케팅 팀이 생겼어. SEO를 봐달래. 근데 CRA는 CSR이잖아? 클라이언트. 사이드. 렌더링. 검색엔진, 봇이 돌잖아. 메타태그? 그 시점에서 없잖아.

막 이벤트 페이지를 만들고, 그 페이지를 공유하면 미리보기 이미지랑 사용자에 따라 다른 QR 코드 같은걸 보여주게 해달래. 으악 이걸 어떻게 해, react-helmet을 끼얹어? 다들 아시다시피 안되잖아요. CSR에서 메타태그를 제대로 다루려면 매우 복잡해지지요. 그래서 예전부터 잠깐 맛만 봐오던 Next.js를 써보려고 했습니다. 근데 App Router라는게 나왔네? 분위기 보아하니 다들 이쪽으로 갈아탈 기세야. 그럼 기존의 Page Router는 레거시 되겠네? 똥 되겠네?

005.jpeg005.jpeg

아니 여러분들도 아시다시피 프론트엔드 쪽은 발전이 엄-청 빠르잖아요? 우리가 배우는 속도보다, 더 빨리 새로운게 나옵니다. 그럼 결국 망할 기술은 안배우고 대세에 편승해야 각이 나오겠지요? 근데, 다들 넥스트 쓰잖아요. 대세네요?

저 사실 넥스트 엄청 싫었거든요. 왜냐, <Link> 컴포넌트 안에 <a>를 집어넣는 형태가 너무 추잡스러웠어요. 근데, 버전업 하면서 그것도 고쳐졌잖아요? 호오 멋-지잖아, 어 거기다가 뭐 넥스트가 Vite.js 보다 빌드가 더 빠르다고? 호오 완-벽하잖아.

그래 넥스트 하자, 나 진짜 마케팅 팀에서 메타태그 달아달라는거 받아내는거 힘들었어.

(발랄하게 과장된 어투) 이번 기회에 나는 최고의 넥스트 빠돌이가 될거야.

넥스트 팀에서 App Router라는걸 내놨다지? Page Router가 좀 칙칙해서 보기 싫었는데 잘 되었네.

006.jpeg006.jpeg

원래 하려고 했던건, 이 화면에 보이는 아이돌 페이지를 Next.js로 포팅하는거였어요. 어쩌다보니까 재미들려서 아이돌도 했다가

007.jpeg007.jpeg

어, 옛날에 SLR클럽 활동할 때 포토샵으로 만들었던 카메라 족보를 웹으로 포팅도 해보고

친구가 홈페이지에 방명록도 만들어보라 해서

008.jpeg008.jpeg

하는 김에 싸이월드로도 만들어보고.. 이거 디자인이 레트로해서 그렇지 뒤에 모눈 같은거 CSS 한줄로 짠겁니다.

009.jpeg009.jpeg

어 또 옛날에 펜탁스 렌즈 계보표 플래시로 만들었던걸 레트로하게 다시 리액트로 만들어보고. 방명록 만들었던 댓글 모델 있으니까 이걸로 댓글도 달게 하고..

010.jpeg010.jpeg

어 보니까 마크다운이랑 댓글이랑 좋아요랑 이미지 업로드 구현했으니 블로그도 가능하겠네, 블로그도 만들어봐야지.. 점점 커지는겁니다.

오 좋아, 넥스트의 온갖 종류의 렌더링 다 만들어볼 수 있겠다. 스태틱, SSR, CSR, SSG, ISG..

신났어, 아무도 날 막을 수 없어.

011.jpeg011.jpeg

넥스트 최신 카나리아 버전, 두시간 전에 업데이트된 카나리아 버전. 하 근데 처음엔 좋았는데 나중 가면서 점점 이상한게 기어나오기 시작합니다.

일단 해시 아이디 링크가 안먹어요. 예를 들어봅시다. 내가 https://miriya.net/idols 에서 해시 아이디 링크를 걸었어요. #ss501 뭐 이런 식으로. 그러면 idols에서 idols#ss501 이렇게 걸려야하잖아, 알아요 에스에스 오공일 말고 더블에스 오공일인거.

11732_4442_1140.jpg11732_4442_1140.jpg

(나름 열심히 준비한 드립이었음)

012.jpeg012.jpeg

근데 좀 뭔가 이상해. idols가 아니라 undefined#ss501로 나오는겁니다. 이것만 문제가 아니야. 링크를 클릭하면 보통 해당 id가 있는 곳으로 스크롤이 점프가 되어야 하잖아요? 근데 페이지가 다시 렌더하는거에요.

뭐야 언제나 그랬듯 내가 잘못한건가? 내가 공식문서를 대충 읽은건가? 공식 문서는 언제 다시 읽어도 생소하지. 근데 문서는 잘 된다고 나와있어.

대체 뭐가 문제지- 그러면 이제 보통 깃허브 이슈에 들어가서 확인해봅니다. 넥스트 js 깃허브 이슈 들어가보니까 사람들이 아우성이여. 하 착한 내가 참아야지.

013.jpeg013.jpeg

막 링크 누를 때 preventDefault 건 다음에 푸시스테이트로 url 바꿔주고, 커스텀 이벤트 쏘고, 이벤트 리스너에선 스크롤 위치 자동 이동하게 해서 아무튼 해결 했습니다. 이걸 이렇게까지 할 일인가 ㅋㅋ; 저기 저 코드가 오직 a 태그를 구현하기 위해 만든거라는게 믿기질 않습니다.

014.jpeg014.jpeg

해시 아이디 링크만 문제여? 쿼리 스트링으로 링크 걸 때도 리랜더가 발생합니다. 또 착한 내가 푸시 스테이트로 바꿔줘야지.. 하.. 이 부분들도 지금은 다 수정된 상태입니다.

015.jpeg015.jpeg

자 다음 문제, 처음에는 모든 페이지를 다 SSR로 Static하게 구현하려고 했어요. 사용자 입장에서는 페이지 하나 통으로 받아서 딱 보여주면 베스트잖아요? 근데 이게 컴포넌트 사이즈가 점점 커지다보니 청크 사이즈 문제 때문에 나눠야겠더라구요. 그래 껍데기 먼저 사용자에게 보내고, 로딩 좀 보여주고, 나머지는 사용자 측에서 렌더링하게 하자. 그럼 뭘 써야돼? next/dynamic을 써야지. 다이나믹 임포트에 SSR: false 해서 잘 넣었어. 근데 뭔-가 이상해.

어떤 페이지에 들어가면, 자꾸만 어떤 js 청크 파일이 404가 뜨는거야. 근데 이게 랜덤해. 단일 페이지에서는 문제가 없었는데, 앱이 점점 커지고 페이지가 늘어나면서, 클릭해서 타 라우트로 이동할때만 문제가 되는거에요. 컴포넌트를 하나하나 주석 처리 해봤더니 문제가 사라졌는데, 진짜 간단한 컴포넌트라도 멀티페이지면 404가 유발되는거야. 이게 알아보니까 client-reference-manifest.json 에 들어가는 정보가 잘못 들어가 있더라구. 이게 한 몇주 후에 13.3.1-카나리 16에서 수정되었습니다.

자, 뭐 여튼 해결은 되었어. 그 다음 문제가 또 나오지. App Route일 경우, CSS 프리로딩이 안돼. 타 페이지로 이동할 경우 FOUC / Flash of Unstyled Content 현상이 유발됩니다. 다들 아시죠, 처음에 흰색으로 나왔다가 스타일이 뒤늦게 적용이 되는거에요. 아니뭐 여태 CRA로 작업할 때는 전혀 문제 없었는데 왜 이럴까? Next.js에서 원래 스타일 파일을 헤더에 붙여넣을 때 preload를 붙여야하는데 안붙인거에요. 그래서 한동안 제 포폴 사이트 남들에게 보여줄 때 매우 쪽팔렸습니다. 그리고 이 문제는 [email protected]에서 수정되었구요.

016.jpeg016.jpeg

그리고 가장 그지같았던 문제는, 특정 훅을 사용할 때 SSR 자체가 그냥 작살이 나버립니다.

원래는 제 페이지를 페이스북에서 공유 할 때 미리보기 og image가 잘 나왔거든요? 근데 어느 순간부터 갑자기 안나오기 시작하는거에요. 이거 원인 찾는게 아주 힘들었는데.. 이 알고 보니까, 예를 들어 URL에 붙어있는 서치 파람스, 그 물음표 붙여넣고 뒤에 오는 놈을 체크할라고 했어요. 그럼 next/navigation에 있는 useSearchParams 훅을 써야 하거든요? 그런데 useSearchParams는 오직 클라이언트에서만 작동을 합니다. 따라서 이놈은 선언된 위치에서부터 위로 타고 올라가면서 Suspense가 있는지 확인을 해요. 근데 없잖아요? 그럼 SSR을 다 개박살 내고 CSR로 로딩하는겁니다. 페이지 메타태그가 안나오는거죠. 아주 소리 없이. 닌자!

이게 실무에서 겪어봤다 쳐봐요. 갑자기 마케팅 팀이 막 난리를 치는거죠. 미리보기가 안나온데. 얼마나 토나와. SEO 관련해서 뭐 점수가 낮게 나와서 페이지 검색결과가 밑으로 가버렸데요, 다 누구 책임이야 내 책임이지. 좀 기다리니까 공식 문서가 추가되더라구요. 경고문도 뜨고. 아 이 손 많이 가는거..

017.jpeg017.jpeg

그리고 프론트엔드 개발자가 포트폴리오를 만들다보니까 백엔드를 너무나 만들기 싫은거에요. 그래서 넥스트에 있는 API 라우트를 이용해서 세미 백엔드 같은걸 만듭니다. 넥스트에서 주는 API 라우트 있죠, src/app/api 폴더 있죠? 파이어베이스 접속이나 이런걸 다 넥스트 API로 돌렸어요. 그러다가 백엔드 쪽이 워낙 좀 껄쩍지근 하다보니까 분리해서 비공개 레포로 돌렸거든요.이후에 파이어베이스가 너무 느려서 걷어내고, 프리즈마에 마리아 디비 해서 아예 버셀에서 벌처로 서버 이전까지 했지요. 무료 서버리스는 다 뒤져야해. 무슨 서버가 응답 속도가 1.5초가 나와요. 그러면서 아 이거 비슷한 타입들이 너무 겹치는데- 하고 백엔드 레포와 프론트엔드 레포를 다시 합치게 되었습니다.

로컬에서는 진짜 잘 돌아갑니다. 그러다가 프로덕션 배포를 할라니까 이게 난리입니다. 배포를 할라면 서버쪽에 데이터를 요청해서 그 데이터로 스태틱 빌드를 해야 하거든요? 근데 백엔드가 프론트엔드랑 합쳐졌잖아? 백엔드가 없지, 그럼 빌드 뻑나지. 그럼 백엔드 배포 안되지, 백엔드 배포 안되면 프론트엔드 안뜨지. 이야 끝내준다. 아니 애초에 그럼 로컬에서부터 안되던가.. 왜 로컬이랑 Vercel에서는 문제 없었는데 벌처로 이전하니까 이러냐고.. 농담이긴 한데 Vercel이랑 Next랑 같은 회사라 그런가?

요즘 한창 구직중인데 면접관이 한 말이 있었어요. 니가 버셀에만 넥스트를 설치해봤으면 제대로 한거 아니라고. 뭐 어쩌겠어요 다시 이 자웅 동체를 프론트엔드랑 백엔드 분리해야지.

018.jpeg018.jpeg

이것만 문제인가, 희한한 문제가 또 있었어요. API 라우트 사용하면 패스별로 GET 쓰던가 PATCH 쓰던가 해서 진짜 서버 처럼 쓸 수가 있잖아요. 어허. 이게 어떤 라우트는 잘 동작하는데, 어떤 라우트는 갑자기 CORS 문제가 발생하는거에요.

019.jpeg019.jpeg

AccessControlAllowOrigin이 보니까 와일드카드로 나오는겁니다. 아니 갑자기 왜? 나는 분명히 넥스트 콘피그에 미리야 닷넷 잘 넣었는데? GET /api/idols/data는 문제가 없는데, GET /api/idols/years만 문제가 있어요.

020.jpeg020.jpeg

얘들이 대단한것도 아녀, 아이돌스 데이터는 그냥 디비 긁어갖고 목록 리턴하는게 다구요, 아이돌스 이얼스도 그냥 디비 긁어갖고 목록 리턴하는게 다에요. 차이라고 한다면 아이돌스 데이터는 GET 말고 POST도 있고, 아이돌스 이얼스는 GET만 있었습니다. 어어어 이거 이거 냄새가 나는데 설마…?

아이돌스 이얼스에 아무 의미 없이 200만 리턴하는 PATCH를 넣었더니 CORS 문제가 사라지네요. GET만 있는 곳에서 문제가 생기기도 하지만, 반대로 PATCH만 있는 곳에서도 문제가 생깁니다. 아이돌 아이디에 PATCH 날려서 해당 아이돌 정보를 수정하는 라우트인데, 이것도 의미 없는 GET이 있어야 정상 동작을 합니다. 진짜 무섭죠, 조용히 엑세스 컨트롤 얼라우 오리진을 와일드카드로 바꿔버려요. 이 문제는 제가 리포트 했는데 아직도 수정이 안되고 있습니다.

021.jpeg021.jpeg

넥스트 앱 라우터의 경우 아직까지 이런 버그들이 많습니다. 굉장히 빠르게 고쳐지고 있지만, 아직은 좀 남아있구요. 심지어는 앱 라우터 나오고나니까 페이지 라우터도 문제가 생겼다는 말도 있습니다. 버셀 사람들이 손이 빨라서, 이 속도로 갈 때 대략 올해 말 쯤 되면 조금 쓸만해지지 않을까 싶습니다.

저는 지금 각종 버그에 대한 해결법을 다 알고 있어서 괜찮은데, 이게 다가 아니잖아요? 여기서 이야기하지 않은 자잘한 잡버그들도 꽤 많습니다. 대부분이 작은 수정으로 해결이 되지만, 아마 여러분중 어떤 사람은 진짜 저보다 더 더러운거 걸려서 뜨거운 맛을 볼 수도 있습니다.

저야 개인 프로젝트라 삽질하고 시간 낭비해도 상관 없는데, 여러분의 조직에서 넥스트를 도입하려고 고민하신다면, 당분간 페이지 라우터를 쓰다가 넘어가시는걸 권장합니다. 두개가 동시에 돌 수 있기 때문에 좀 아니다 싶으면 페이지로 빼버려도 되거든요.

어 그리고 넥스트에서 이야기하는 ISR 있죠, Incremental Static Regeneration.. Revalidate 요청 날리면 서버에서 다시 빌드해주고 하는 그거요. 이것도 지금 이슈에 댓글이 무지하게 달리고 있습니다. 제 개인적인 의견이지만, 넥스트는 그냥 CSR로 사용하면서, 껍데기만 Static으로 만들어서 메타태그 정도나 보여주는게 안전빵인것 같습니다. (특수한 페이지 빼고)

넥스트 문서 읽어보면 프론트엔드의 새로운 패러다임이 펼쳐질것 같지만, 많은 부분들이 아직 오작동하고, 제대로 구현되지 않은 부분이 있습니다. 이 점을 염두해두시고, 부디 여러분의 개발은 저와 달리 꽃길만 걸으시길 바랍니다. 감사합니다.


후기

지난 몇달간 여러분이 보고 계신 이 화면 만드느라 넥스트 써보면서 삽질한 내용들을 모아 만들어봤습니다. 이거 발표 자료 준비하고 15분 맞춘다고 여러번 타이머 걸어놓고 읽었는데, 막상 발표하게 되니 정신이 없었네요. 제가 가장 걱정하는건 제가 막 나대거나 으스대는 느낌으로 들릴까봐... 가끔 웃어주시는 부분들이 있었는데 진짜 재미있어서 웃은건지 사회적으로 웃어준건지는 모르겠습니다. :( 떨려...

저는 항상 먼지 같은 놈이라고 생각하며, 제가 짠 코드 보다 좋은 코드가 있을 수 있다고 생각합니다. 2주 전에 짠 코드가 똥으로 느껴지지 않는다면 저는 성장이 멈춘거겠죠. 언제나 제 글이나 생각에 대해 틀린점 지적이나 다른 생각, 혹은 뭐든 간에 댓글로 남겨주시면 저는 감사하게 읽겠습니다. 다 제 발전을 돕는 자극이 됩니다.

읽어주셔서 감사합니다.