리액트 프론트엔드 개발자 입장에서 프로젝트 단계별로 일어나는 일 정리해보았습니다.
지난 2년 동안 프론트 개발해보면서 겪으며 경험해본 것들을 정리한 것이겠네요.
프로젝트 시작단계
프로젝트에 무슨 기술을 사용할 지 정하게 됩니다.
- 이미 서비스중인 프로젝트에 사용하던 익숙한 기술을 그대로 가져와서 적용하는 게 가장 빠르고 쉬운 길입니다.
- 그러나 신기술을 도입하기에 이 단계보다 더 쉽고 간단한 단계는 없습니다.
- 또한 기존에 사용하던 기술에서 느꼈던 불편함을 어쩌면 신 기술 도입으로 개선할 수도 있습니다.
이전 프로젝트에서 사용하던 기술에서 무엇이 불편했는지 정리하고 신기술 탐색 및 기존 불편함 점을 해결할 수 있을지 검토해야 합니다.
- 예를 들면, 기존 CRA 프로젝트에서 서버사이드 기능이 많이 필요해서 별도 프론트엔드용 서버를 구축하고 운영하면서 어려움을 겪었다면 자체적으로 서버가 통합되어 있는 Next.js 프레임워크를 통해 해결할 수 있는 경우가 있겠습니다.
또한 적용하려는 기술이 신규 프로젝트의 성격에 맞는지도 확인하는 것도 중요한 듯 합니다.
- 예를 들면, 특정 도메인 데이터에 대해 굉장히 많은 부분 쿼리가 예상된다면, graphql 도입이 적절할 수 있습니다.
- SEO 최적화 중요도가 높다면 CRA 보다 Next.js 가 나은 선택일 수 있습니다.
동료와의 협업 방식을 정하는 것도 중요합니다.
- 코드 컨벤션, eslint, prettier설정, 개발 문서 정리 등 생각할 것들이 꽤 많습니다.
- 사실 이 단계에서 많은 의견 충돌이 발생하는 것 같습니다... 의견 충돌은 많을 수록 프로젝트 시작은 늦춰집니다. 의견 충돌 발생하는 건 개발자 간의 코딩 스타일이 다르기 때문인 것 같은데요. 자기 만의 스타일과 습관이 있어서 규칙을 정해둔다고 해서 사실 지키기 쉽지 않다고 생각됩니다.
이걸 강제로 규칙을 지키게 하는건 아침에 6시에 무조건 기상하세요. 7시반 까지 출근을 하세요. 매일 1시간 운동하세요. 라고 하는것과 크게 다를 바 없는 것 같아서 오히려 자율성을 해치는 것 같습니다. 이렇게 생산성 저하 등의 부작용이 생기기 마련입니다. - 결국에는 정말 큰 틀에서의 규칙만 정해서 지키면 되고 (한글 변수명 사용해도 되느냐? 타입스크립트 타입명에 I 붙일것이냐? 등등) 그 외의 부분은 각자에게 맡기는게 좋은 것 같습니다.
프로젝트에 페이지가 많아지고 파일이 많아지는 단계
갈 수록 페이지 수가 많아지고, 컴포넌트들이 복잡해집니다.
어던 기능이 다른 곳에서도 사용되게 되는 상황이 자주 일어납니다.
폴더 구조와 파일 정리가 필요해지는 순간입니다.
이렇게 되는 이유중에 하나는 미래를 알 수 없어서, 기획이 자주 바뀌어서 그런 것 같습니다.
이부분은 정답이 없는 것 같은데 프로젝트 규모와 상태에 따라서 달라집니다.
초기에는 components 폴더에 다 때려넣어도 상관없다고 생각합니다.
hook 도 hook 폴더에 다 때려넣고, 함수도 utils 파일 하나에 다 때려넣어도 상관없습니다.
components
ㄴhook
ㄴutils
ㄴ컴포넌트...
ㄴtype.d.ts
그러다 도메인이 나눠지고 기획 틀이 잡히게 되면, 각 도메인에 해당하는 폴더 아래에 정리가 필요하게 되는데
특정 도메인에만 해당하는 타입, hook, 함수 같은 것들은 그 도메인 폴더 아래에 위치해야 되는게 편했습니다.
글로벌 하게 관리하는 건 거의 프로젝트에 종속적인 것들만 남겨두었습니다.
components
ㄴdomain
ㄴhook
ㄴutils
ㄴ컴포넌트...
ㄴtype.d.ts
ㄴdomain
ㄴhook
ㄴutils
ㄴ컴포넌트...
ㄴtype.d.ts
컴포넌트 나누는 것도 필요하게 됩니다.
- 프로젝트 초창기에는 컴포넌트를 나누지 않고 하나로 유지하는게 좋습니다. 어떻게 변할 지 모르니까요.
- 규모가 커지면 결국 가독성 때문이라도 나누는게 좋다고 생각합니다. 한 파일에 너무 많은 코드가 있으면 위에서 부터 아래로 읽은 것 자체가 너무 괴로워지는 순간이 오거든요. useEffect 가 도대체 몇 개가 들어가는 것인지 따라가기가 어려운 순간이 옵니다.
- 개인적으로는 가능하면 하나의 컴포넌트를 스크롤 몇 번 안해도 다 볼 수 있는 정도로 유지하려고 노력합니다.
hook 나누기
- 무작정 모든 로직을 hook 으로 다 빼버리면 나중에 맥락 이해할 수 없는 상태로 코드 양만 줄어드는 불상사가 일어날 수도 있습니다. state와 useEffect 가 컴포넌트에 노출되지 않고, hook 안으로 숨어들어가기 때문에 하나하나씩 들어가서 까봐야 맥락 이해할 수 있게 되는 것 같습니다. 그래서 거대한 코드 양을 줄이기 위한 목적으로만 hook 으로 빼내는 건 어쩌면 양날의 검 같은 느낌입니다. 좋지만 안좋은? 🤔
- hook 의 원래 용도대로 재사용 가능한 독립적인 기능을 hook 으로 빼내야 맥락 이해 못하는 상황을 줄일 수 있는 것 같습니다.
- 저는 최근에는 긴 도메인 로직을 줄여야 한다면, state, useEffect 는 컴포넌트에 그대로 노출하려고 노력하고, 기능을 단박에 이해하기 쉽게 이름을 잘 붙인 "함수"로 분리하는 방향으로 개발하고 있습니다. hook 을 너무 많이 사용하지 않으려고 합니다.
상태관리 라이브러리
- 여러 컴포넌트에서 상태를 공유해야 되는 상황이 생깁니다. props 를 무한정 아래로 전달전달 하는 방식에서 벗어날 때가 되었습니다.
- 대표적인 redux, mobx, recoil, 최근에 핫한 zustand, joti 등 많은 상태관리 라이브러리 있습니다. 글로벌 상태 스토어에서 바로 가져다 쓰는 방식이라서 편하다는 장점 있지만, 가끔 너무 복잡해지면 흐름을 이해하기 어려울 때도 있더군요. 여기서 상태가 바뀌면 어디어디가 영향을 받는 것이지...? 이런 것들요..
- react-query 나 apollo client 같은 응답값 캐시 관리 기능을 지원하는 라이브러리 사용하면서 부터, 서버에서 받은 응답 상태와 UI 상태를 구분하게 됩니다. 서버 응답 상태는 굳이 상태관리 라이브러리에 넣을 필요가 없거든요.
사실 프로젝트 초창기부터 react-query 또는 apollo client 를 바로 사용하면서, UI 상태만 상태관리 라이브러리에 저장하는 식으로 개발을 해야 나중에 리펙토리 하면서 머리 아플 일이 없을 듯 합니다.
성능 최적화가 필요한 단계
규모가 왠만큼은 커져야 고민이 되는 단계인 듯 합니다. 대략 최소 1년 이상 개발 ??
자바스크립트 청크 파일이 비대해지고, 렌더링 속도가 느려지고, 빌드 속도가 느려지고... 많은 느려짐을 만나게 됩니다.
렌더링 속도 개선을 시도합니다.
- 비효율적인 로직을 효율적으로 개선합니다. 프론트엔드는 알고리즘 공부가 뒷전이긴 하지만, 이럴 때 문제 해결 사고 방식이 크게 빛을 발휘합니다.
- 무거운 로직을 useMemo hook 으로 메모이제이션 시키고, 불필요하게 매번 함수가 다시 만들어지지 않도록 useCallback hook 을 사용합니다.
- 렌더링시 스타일로 인한 부하를 최소화 시키기 위해 css-in-js 를 tailwind 와 같은 utility-first css 로 교체 시도합니다.
First Load JS 를 최소화 시키고, html 파일 크기를 줄이기 위해 노력합니다.
- 자바스크립트 청크 파일을 분할하게 위해 리액트의 Lazy, Next.js의 dynamic import 등을 사용하여 동적 분할을 시도합니다.
- 불필요한 코드를 정리합니다.
네트워크 요청 횟수를 최소화 하고, 캐싱 전략을 극대화 시킵니다.
- 여러 요청을 한번에 보내는 경우 Promise.all, Promise.allSettled() 등을 사용하여 병렬 요청을 보냅니다.
- React Query(tanstack Query), Apollo Client 등의 라이브러리를 통해 손쉽게 응답 캐시를 관리하고, 캐싱된 값을 적극적으로 재활용합니다.
이미지 최적화 시도합니다.
- 너무 큰 이미지 파일의 용량을 줄이는 시도합니다. 특히 클라이언트에서 webp 확장자를 지원할 경우 더 용량이 적은 webp 확장자 이미지를 다운로드 하도록 합니다. Next.js 는 Image 컴포넌트로 자동으로 해줍니다. CRA 환경은 개발자가 직접 해줘야 합니다.
- 모바일/데스크탑 환경, 화면 너비에 따라서 적절한 크기의 이미지를 다운로드 받도록 설정해줍니다. 예를 들어 모바일 화면에서는 너무 큰 이미지는 불필요하기 때문이죠.
- svg 를 img 테그나 Next.js Image 컴포넌트로 사용시 캐싱 안되기 때문에 이미지의 캐싱을 위해 svgr 과 같은 라이브러리를 사용하여 컴포넌트로 변환하여 사용합니다. svgo 를 통해 svg 파일의 최적화도 가능합니다.
라이브러리 교체를 시도합니다.
- bundle-analyzer 를 설치하여 번들 분석합니다. 너무 지나치게 큰 용량을 차지하는 라이브러리를 찾아냅니다.
- 같은 기능을 구현가능한 더 작은 용량의 효율적인 라이브러리를 찾아서 교체합니다.
- 혹은 라이브러리 release 노트를 보고 개선된 라이브러리 버전으로 업그레이드 합니다.
import 최적화 합니다.
- 대표적으로 lodash 가 있는데요. lodash 패키지 전체를 import 할 것이냐, 아니면 lodash 패키지의 일부분만 import 할 것이냐 이런 것에 대한 최적화 입니다. 가능하면 필요한 부분만 쓰는게 좋겠죠.
- Next.js 에는 2가지 전략이 있습니다. modularizeImports, optimizePackageImports(Next.js v13.5 이상)
빌드 속도 개선
- 너무 많은 라이브러리를 설치하여 빌드 속도가 너무 느려진 경우에는 npm, yarn 대신에 pnpm 과 같은 패키지 관리자 도구를 도입하는 것도 생각해봐야 합니다.
- webpack 대신에 vite 와 같은 최신 모듈 번들러의 도입도 괜찮습니다. 예전에 vite 는 아직 이르다는 게시물을 작성했었는데 조금만 더 참고 해볼걸...후회되네요. 최근에 동료분께서 적용 성공하신것을 보고는 빠른 성능에 감탄하고 말았는데요. 개발 환경에서의 압도적인 로딩 속도를 자랑하고, 빌드 속도도 상당히 많이 단축됩니다. 요즘은 아예 CRA 팀에서도 CRA 대신에 vite 써라고 하더군요. 😂
서비스 런칭 단계
테스트 코드 작성
- 여기서 말하는 테스트 코드는 e2e(end-to-end) 테스트로 실제 유저가 사용한다고 가정한 테스트를 뜻합니다.
- 테스트 코드 작성은 선택이 아닌 필수입니다. 그런데 "언제" 하느냐가 중요하죠. 예전에 테스트 코드 주도 개발이라고 TDD 가 핫한 주제로 떠올랐던 적이 있었지요. 사실 저는 지금도 TDD 는 허무맹랑한 판타지라고 생각하긴 합니다. 왜냐면 다음날 출근하면 기획과 디자인이 바껴있는걸요. 🤣🤣🤣 아예 외주 받아서 기획서랑 디자인 시안이 정해져있는 SI 개발에서는 괜찮을 거 같네요.
그러면 테스트 코드 작성 언제 하느냐? 저는 서비스 런칭 전후로 하는게 맞다고 생각합니다. - 물론 바쁘면 테스트 코드는 뒷전이 될 수도 있습니다. 당장은 로컬에서만 돌아가도 괜찮고, 배포 환경에서도 테스트 할 수 있도록 개선이 필요합니다.
- 테스트를 CI/CD 통합하는 방법도 있습니다. 배포 전 또는 후에 테스트 코드를 수행하여 검증하는 것이죠. 근데 자동화로 만드는 방법이 있고, 수동으로 테스트 수행하는 방법이 있는데. 제 경험상 신규 기능은 없고, 가끔 유지보수 하는 서비스는 배포 전에 테스트 코드 자동으로 돌게 하는게 좋았습니다. 그게 아니라 현재 진행형으로 바쁘게 개발되는 서비스는 그냥 수동으로 한번씩 테스트 코드 돌려서 검증하는게 좋았습니다.
seo 최적화
- B2B 서비스나 사내 백오피스 서비스가 아니라 일반 사용자들이 검색 엔진을 통해 접근해야 하는 B2C 서비스의 경우 seo 최적화가 매우 중요하게 됩니다. 프론트엔드의 꽃이라고 볼 수 있습니다.
- CRA 프로젝트의 경우 seo 대응을 위해 별도 서버를 통해 index.html 파일을 조작해야합니다. 크롤링 봇이 각 페이지 경로에 대해 접속시 필요한 정보를 읽을 수 있도록 meta 테그를 삽입하여 index.html 파일을 조작해야합니다. 또한 동적 페이지가 다수 존재한다면 사이트맵 생성이 필요합니다.
- Next.js 프로젝트의 경우 서버사이드 렌더링을 활용할 수 있겠습니다. 동적 페이지에 대해서 SSG, SSR 를 사용하면 되는데 SSG 를 사용하면 빌드 단계에서 페이지의 html 파일 만들기 때문에 압도적으로 빠른 응답속도를 가지게 되고, 페이지 접속할 때마다 네트워크 요청 보내지 않아도 된다는 장점 있습니다.
그러나 페이지 수가 기하급수적으로 많으면 모든 페이지에 대한 html 파일 만드는건 현실적으로 불가능해서 SSR 을 사용해도 충분히 대응이 가능합니다. 가능하면 html 파일의 크기는 줄이는게 좋다고 하여, 꼭 필요한 내용만 담아서 크롤링봇에 전달하는게 좋다고 합니다.
사이트 등록
- 구글 에널리틱스, 테그 매니저 설정 필요할 수 있습니다.
- 구글 서치콘솔, 네이버 서치어드바이저에 사이트 등록 해야합니다. 사이트맵 제출을 위해 사이트맵 제작이 필요합니다. 정적 페이지는 괜찮은데 동적 페이지에 대한 사이트맵 제작을 위해 사이트맵 생성 코드 작성하여 로컬에서 수행하고, 프로젝트 public 폴더에 넣어서 배포시킵니다.
- 그 외에 네이버,카카오 등의 서드파티 서비스를 운영 환경에서 사용하기 위해 설정 필요할 수 있고 검증을 받아야 할 수 있습니다. 특히 모바일 앱 개발이라면, 앱스토어의 검토기간이 상당히 길다고 하는데요. 이런 것들도 생각해두어야 할 듯 합니다.
분산 추적/모니터링 도구를 설치하여 어디서 어떤 에러가 발생했는지 추적하여 해결할 수 있어야 합니다.
- santry, elastic 등 수 많은 솔루션이 있는데 회사에서 사용하던 걸로 진행하게 되겠지요. 실시간으로 에러를 확인하는 방법을 숙지해야하고, 알림 설정을 해야 될 수 있습니다.
서비스 내에서 버전이 나눠지는 단계 (환경에 따라 다른 동작)
같은 페이지에 여러 도메인 주소가 존재하게 될 수 있습니다.
- dev, stage, prod 와 같은 환경이 나눠져 있다면 그럴 겁니다.
- 도메인에 따라서 다르게 처리하는 로직이 추가될 수 있는데, 도메인 주소를 확인해서 현재의 환경을 결정짓는 상수를 만들고, 이런 것들을 하나의 파일에서 관리하는게 유지보수가 쉽습니다.
유저 정보에 따라 조건별 동작
- 도메인으로 구분하기 보다는 로그인한 유저 정보에 따라 스타일을 다르게 하거나, 다른 동작을 하게 로직을 짜거나 해야할 수 있습니다.
- casl 라이브러리 같은 것을 사용하면 rule-base-access control 을 구현할 수 있는데요. 사실 그렇게 추천하지는 않지만, 어떤 경우에는 적합할 수 있을 것 같습니다.
- 예를 들어서 A,B,C 조건을 만족하는 경우 a 기능을 쓸 수 있다. 이런 기능을 구현해야 한다고 가정하겠습니다. 그러면 해당 유저 정보를 가져와서 조건문 A,B,C 조건을 만족하는가? 확인해서 통과하면 a 기능을 사용하겠죠. rule-base-access control 를 사용하면 해당 유저가 로그인하는 순간 a 기능을 사용할 수 있는지 rule 을 부여하는 것입니다. 즉, 코드가 if(A && B && C) 이면 a 가 아니라 if(aRule === true) 이면 a 이런 식으로 바뀌는 것이죠.
- 이게 뭐가 다른거냐 한다면, 모든 코드마다 userInfo 가져와서 A&&B&&C 이렇게 조건 비교하지 않고, aRule 이 true 인지만 확인하면 된다는 점입니다. 즉, 변경에 매우 유연하게 대응가능하게 됩니다.만약 해당 유저가 a 기능을 못쓰게 바꿔야 한다는 요청이 들어오면 Rule 만 바꿔주면 된다는 겁니다.
http, https 프로토콜에 따라 동작
- 사설망 환경을 지원해야 하는 특수한 케이스 입니다.
- cdn 에서 가져오는 글꼴폰트 같은 것들을 못쓰기 때문에 사설망 환경용 설정을 별도로 해주어야 합니다.
- protocol 이 http 인가 https 인가에 따라서 조건부 처리가 들어가야 합니다.
- 사실 제일 어려운 점은 사설망 환경에서의 테스트가 매우 제한적이고 어렵습니다. 고객사가 사설망 환경일 때....특히 그렇습니다...🥲
여러 개의 서비스가 만들어지는 단계
서비스가 늘어나면서 각 프로젝트 마다 투자할 수 있는 시간은 크게 줄어듭니다. 많아지면 많아질 수록 관리가 안되기 시작합니다.
가능하면 이미 있는 프로젝트 안에서 추가하여 해결하려고 시도해야 하며, 프로젝트를 나누는건 이유가 합당한지 타당한지 다시 한번 깊게 생각해봐야 합니다.
서비스 별로 repo 가 분리되어 있는 경우 만날 수 있는 여러 필요성이 존재합니다.
UI의 일관성과 재사용성을 위해 공통된 디자인 시스템을 공유해야할 필요성이 생깁니다.
- 별도의 라이브러리를 만들어서 각 서비스에 설치하여 사용하는 것도 좋은 방법입니다. 그러나 별 것 아닌 코드 수정을 한번 하려면, 코드 수정하고 배포하고, 각 서비스에 라이브러리 버전 업데이트 해주고 반영해주는 과정을 겪어야 한다는 거죠. 이게 의외로 엄청 불편합니다. 버전 관리도 어느순간 안되게 되더군요. (서비스 마다 디자인시스템 라이브러리 버전 신경써서 업데이트 안해주면 다 달라짐...) 라이브러리 관리가 진짜 어렵습니다.
- tailwind 를 쓰면서 서비스별로 config 파일을 최대한 일관성 있게 가져가면, 어느정도 디자인시스템을 유지할 수는 있는 것 같았습니다. 아직은 저도 뭐가 제일 좋은 방법인지 확답을 내리지 못했네요.🤔
타입을 공유해야 할 필요성도 생길 수 있습니다.
- 한때는 서브모듈을 사용해서 프론트-백엔드 간의 타입 공유를 도입해본 적 있었으나, 이것도 디자인시스템 라이브러리 처럼 타입 한번 바꾸려고 많은 과정을 겪어야 한느 불편함이 있어서 폐기 해버렸죠.
- 지금은 프론트-백엔드 타입 공유에 대해서는 codegen 도구가 정답이라고 생각합니다. 백엔드에서 작성해둔 타입 기준으로 프론트엔드에서 자동으로 타입을 가져오는 방식이라서 사실상 이것보다 편한 방법 없다고 봅니다.
모노레포는 어떨까요?
- 하나의 레포에서 공통 UI와 함수들을 여러 개의 서비스에서 공유하는 획기적인 아이디어 입니다.
- 한때는 모노레포를 사이비 종교처럼 숭배하기도 했는데 지금은 모노레포 해야되냐? 물어본다면, 회사규모가 어마무시하게 커서 붕어빵 처럼 같은 페이지를 엄청나게 찍어내는 상황이 아니면 안하는게 좋다고 대답할 듯 합니다...모바일-데스크탑 크로스 플랫폼 개발 같은 정말 특수 상황에서만 써야하는 것 같기도 합니다.
서비스 통합에 대한 필요성이 있을 수 있습니다.
- 하나의 통합 페이지가 존재하고, 텝으로 구분된 여러 개의 서비스가 존재하는 경우가 이에 해당합니다. 각 서비스는 repo 가 다르고 주소도 다릅니다. 그런데 로그인은 유지되어야 한다. 이런 복잡한 상황인 것이죠.
- SSO 통합 로그인을 구현하는 방법이 있겠습니다. 토큰 값을 넘겨서 인증 상태를 이어가는 것이라고 생각하면 됩니다. 그런데 도메인이 달라지는 경우도 있는데 이런 경우가 진짜 머리 아프더군요. 도메인이 달라져서 백엔드에서 쿠키 설정을 못하게 되는 그런 경우 만나 봤습니다... 결국 도메인 주소는 동일하게 맞춰주는 게 젤 편한 방법입니다.
- 별도 인증 페이지를 만드는 것도 좋은 방법입니다. 대표적으로 네이버와 카카오가 어느 서비스에서 로그인 시도하든 별도의 로그인 페이지에서 로그인을 하는 것 처럼..
로컬 포트를 정리해야 합니다.
- 서비스를 통합한다면, 로컬에서 여러 서비스를 동시에 실행해야 될 수 있습니다. 이럴 때 로컬 포트를 기본 3000 포트로 고정해두면, 포트 충돌이 발생해서 작업할 수가 없습니다. 미리미리 공용 개발 문서에 서비스 별 포트 번호를 작성하여 정리해야하고, 동료들에게 공유해야 합니다.
적다보니 끝도 없는데 더 생각나면 내용 추가하겠습니다.
'개발자 이야기' 카테고리의 다른 글
"개발자가 학습을 중단하는 방법: Expert Beginner의 부상" 리뷰 (2) | 2024.11.03 |
---|---|
지난 6개월 되돌아보기 (이사, 경매, AI) (1) | 2024.06.12 |
22년, 23년 가장 많은 애정을 쏟았던 곳, 24년은? (2) | 2023.11.25 |
4-5월 재충전의 시간 (걷기, 익명의 편지 모임) (2) | 2023.06.05 |
몸도 다치고 마음도 다치고... 좋은 일이 일어나면 그 다음은 나쁜일이 아니라 아주 나쁜일이 일어나는 요즘 근황 (1) | 2023.04.16 |