-
Notifications
You must be signed in to change notification settings - Fork 4
고민과해결
J213_한범석 edited this page Dec 5, 2021
·
3 revisions
- 탄탄한 그라운드룰과 견고한 협업 문화를 설정하여 팀원 간의 소통이 수월하게 이루어질 수 있도록 했습니다.
- PR은 리뷰가 있어야 Merge 할 수 있도록 하여, 서로의 코드를 리뷰하고 프로젝트 진행 상황을 파악할 수 있도록 했습니다.
- 매주 월요일 스프린트 계획, 매일 아침 스크럼을 통해 프로젝트의 진행 상황을 모두가 공유할 수 있도록 했습니다.
- 슬랙에서 GitHub를 구독해 PR, Issue의 알림을 받고 즉각 확인할 수 있도록 했습니다.
- 슬랙과 노션을 활용하여 Backend, Frontend 소통할 수 있는 구역을 각각 만들어 커뮤니케이션에 드는 리소스를 최소화 했습니다.
발단
- 타임라인 페이지, 랜덤 페이지, 프로필 페이지에 표시될 게시글 데이터를 전역 상태로 관리했습니다.
- 각 페이지를 로드하는 시점에 React query를 통해 필요한 데이터를 Fetch하고, 상태에 저장했습니다.
문제
- 다른 페이지로 이동하는 경우 데이터가 Fetch되기 전까지, 이전 페이지에서 저장되었던 데이터가 렌더링 되어 좋지않은 사용자 경험성을 야기했습니다.
해결
- Props를 한 단계만 내려주면 되었기 때문에 전역 상태로 게시글 데이터를 관리하지 않았습니다.
발단
- Server side 에서 실행되는 함수인
geServerSideProps
함수에서 Access token으로 인증된 유저 정보를 요청하고, 각 Pages component 에서useSetRecoilState
훅스를 통해userAtom
에 유저 정보를 저장했습니다. - Recoil 상태의
setState
는 비동기 작업으로 처리되었고, 20ms ~ 50ms 정도의 시간이 걸렸습니다. - Recoil 상태를 사용하는 컴포넌트는 초기 렌더링 한번, 상태가 저장되고 한번 총 두번 렌더링되었고, 그 사이에 간극은 사용자 경험성을 떨어뜨렸습니다.
- 유저 정보가 즉시 필요한 컴포넌트는 Props로 넘겨주었고, 그렇지 않은 컴포넌트는 전역 상태값을 사용했습니다.
문제
- Props drilling 등의 문제로 인해 복잡도가 증가했고, 그렇다고 모두 상태가 저장될 동안 렌더링을 지연시키자니 Server side rendering의 장점을 버렸습니다.
해결
-
RecoilRoot
컴포넌트에서initializeState
Props로 상태 초기데이터를 설정할 수 있는 것을 발견했습니다. - 이 함수를 활용하여 초기의 상태를 저장한다면 5ms ~ 30ms의 비동기적 작업없이 동기적으로 처리되는 것을 확인했습니다.
노력
- 타입스크립트를 적용하며 많은 타입 오류를 마주하게 되었고 그때마다 발생한 오류에 대해서 학습을 진행했습니다.
성장
- 타입 스크립트를 사용하면서 변수의 타입에 대해 얼마나 신경을 쓰지 않고 작업을 했었는지 알 수 있었습니다.
- 정적 타입을 통해 컴파일 단계에서 오류를 포착할 수 있어 미연에 실수를 방지할 수 있습니다.
- 완벽에 가까운 코드 자동완성을 경험하며, 'IDE는 참 똑똑하구나'라고 느꼈습니다.
- 이젠 타입스크립트가 없으면 안되는 몸이 돼버렸습니다.
고민
- SNS 서비스 초기 유입 유저의 경우 팔로우한 사람이 없어 보여줄 컨텐츠가 없었습니다.
- 초기 유입 유저의 참여도를 높이고 서비스에 더 적극적인 참여를 유도하고 싶었습니다.
해결
- 팔로우한 사용자가 0명인 유저에게 랜덤으로 사용자를 추천해주어 팔로우를 쉽게 할 수 있도록 했습니다.
- 다른 사용자를 팔로우 할 경우 해당 사용자의 포스트를 타임라인에 실시간으로 반영시켜 서비스 이용 흐름이 끊기지 않도록 했습니다.
고민
- 잦은 API 호출과 큰 데이터의 송수신 간에 타협점을 찾고싶었습니다.
해결
- 멘토님의 '10000개와 10개의 비교가 아니라면 차이가 없을 것이다.' 라는 답변을 듣고, 사용자 경험성에 집중하여 불러올 데이터의 개수를 책정했습니다.
발단
- 몽고DB Schema 설계하며 Read 에 유리한 임베드 방식을 많이 채택했습니다.
- 몽고DB 를 학습하며 임베드 방식으로 설계한 Schema 를 래퍼런스 방식으로 변경해야 된다고 느꼈습니다.
문제
- SNS 특성상 확장이 자주 일어나는 Schema가 존재합니다.
- 몽고DB 에서 제공되는 Document 의 기본 최대 크기는 16mb 로 가변적인 성격의 데이터 유형들이 하나의 Collection 으로 임베드 되어 있다면 16mb 를 초과할 수 있습니다.
- 다양한 유형의 데이터가 임베드된 Collection 에는 많은 Index 적용이 필요합니다. 많은 Index 가 적용된 Collection 은 Index cardinality 가 낮아지며 쿼리 플래너로 인하여 잘못된 Index 가 선택되어 성능이 저하될 가능성이 높아집니다.
결론
- 데이터의 성격이 가변적인 성격이 강하며 Read 작업의 빈도수가 높아 Index 적용이 많다면 임베드 방식보다는 레퍼런스 방식을 고려하며 적용했습니다.
문제
- 패턴을 고려 안하고 구현시 하나의 파일이 명확한 기능을 가지지 못하고 길어지며 가독성이 떨어졌습니다.
- 코드 확장에 따른 개발 코스트가 많이들어가는 일이 빈번히 발생했습니다.
해결
- 확장성과 유지 보수를 위하여 3 Layer architecture 를 적용 했습니다.
- Router, Service, Model 로 분리하여 한 파일의 코드가 너무 길어지거나 가독성이 떨어지는 경우를 최소화 했습니다.
- 다른 계층에 영향을 미치지 않고 확장, 수정이 용이하여 개발 코스트를 아낄 수 있었습니다.
욕심
- SNS 서비스의 특성상 검색엔진 최적화가 필수 중 필수라고 생각했습니다.
- 인증이 필요한 페이지를 제외하고 최대한 많은 페이지를 SEO 하고 싶었습니다.
노력
- 포스트 전체 보기 페이지를 SSR로 만들었습니다.
- 유저 프로필 페이지를 SSR로 만들었습니다.
- 유저 대쉬보드 페이지를 SSR로 만들었습니다.
- 검색 상위 노출을 목표로 하기 위해 접근성을 높였습니다.
- 오픈그래프, 디스크립션, 키워드 등 다양한 메타태그를 활용했습니다.
- 지푸라기라도 잡는 심정으로 구글 서치 콘솔에 도메인을 등록했습니다.
결론
- 아직 검색 노출이 되진 않았지만, 조만간 많은 노출이 있을 것이라 확신합니다.
발단
- 보안을 위해 팀원의 IP만 SSH 접속을 허용해두었습니다.
- Main 브랜치에 Push가 됐을 때 GitHub actions 을 통해 배포하도록 설정했습니다.
문제
- 하지만 GitHub actions 이 실행되는 GitHub hosted runner 의 IP는 매번 달랐고, 이를 모두 White list에 포함하는 것은 불가능했습니다.
해결
- GitHub Action의 job을 커스터마이즈한 환경에서 실행할 수 있도록 해주는 GitHub self hosted runner 라는 것을 알게 됐습니다.
- 서버와 클라이언트 인스턴스에 각각 Self hosted ruuner를 pm2로 실행했고 action이 발생하면 각 인스턴스에서 배포 Action이 실행됩니다.
결론
- 보안도 챙기고 CI / CD도 챙겼습니다.
발단
- 함수 또는 변수를 Memoization 해주는 hooks 이기 때문에 무조건 쓰는 것이 좋다고 생각해서 매번 사용했습니다.
문제
- Memoization 하기 위한 리소스와 hooks 는 호출되는 비용으로 인해 속도가 빨라지기는커녕 느려지는 것을 발견했습니다.
해결
- 두 hooks를 적절하게 사용하기 위해 사용했을 때와 사용하지 않았을 때를 프로파일링했습니다.
- 수십개의 값과 콜백을 프로파일링 하다 보니 사용하는 것과 사용하지 않는 것을 선택하는 요령이 생겼습니다.
- Text input alue 처럼 상태가 빠른 속도로 여러 번 바뀔 때 이외에는 사용하지 않는 방향으로 진행했습니다.
결론
- 컴포넌트의 크기와 상황에 따라 결과는 달라질 수 있으므로 퍼포먼스가 중요한 경우에는 프로파일링이 필수라는 것을 배웠습니다.
문제
- 소셜 인증을 완료하고 난 뒤 루트 경로로 리다이렉트 되도록 했습니다.
- 루트 경로가 아닌 다른 경로에서 인증을 할 수 있는 상황도 생겼습니다.
- 다른 경로에서 인증하고 루트 경로로 리다이렉트 되는 것은 유저 경험성이 떨어지는 흐름이라고 판단했습니다.
해결
- 인증을 요청하는 URL에 쿼리 파라미터로 리다이렉트 되고싶은 곳을 넣어서 요청했습니다.
- 요청을 받은 서버는 요청 URL의 쿼리 파라미터에 있는 리다이렉트 값으로 리다이렉트 해줍니다.
결론
- 클라이언트에서 리다이렉트 될 경로를 커스터마이즈할 수 있게 되어 만족도 높은 유저 경험성을 제공할 수 있었습니다.
의문
- 컴포넌트의 props 타입체크에서 PropTypes와 Typescript를 동시에 사용했습니다.
- 문득 '둘 중 하나만 있어도 되지 않을까?' 라는 고민을 했습니다.
해결
- 조사 결과 Typescript는 컴파일 시에, PropTypes는 런타임에 타입 체크를 한다는 것을 알게 됐습니다.
욕심
- 사용자의 회원가입을 Social 인증을 통해 간편화 하고 싶었습니다.
- 서비스 사용자가 이용하는 GitHub, 블로그 등 다양한 서비스의 활동들을 연동을 통해 제공하고 싶었습니다.
고민
- 연동되는 서비스에 대해서 사용자 검증이 필요했습니다.
- 사용자 검증이 완료 되었다면 연동되는 서비스의 활동을 가져오는 방식에 대해서 고민했습니다.
- 서비스의 활동을 가져오는 크롤링 때문에 Cost 가 많이 들었습니다.
해결
- Open API 를 제공하는 서비스 Tistory, Kakao, Google, GitHub 의 경우 OAuth2 방식을 이용하여 사용자를 검증하고 각 서비스의 식별자를 데이터베이스에 저장하여 사용했습니다.
- 우선적으로 서비스에서 제공하는 Open API 을 다양한 활동 내역을 얻을 수 있었습니다. 하지만 API 로 제공되지 않는 내역에 대해서는 크롤링을 통해 원하는 활동 내역을 가져와 사용자에게 제공했습니다.
- 빈번한 크롤링을 방지하기위해 가져온 활동 내역은 데이터베이스에 저장해 사용합니다.
- 활동 내역 갱신을 위해서는 사용자의 상호작용을 통해 크롤링을 다시 진행하여 데이터베이스를 갱신하는 방법을 사용했습니다.
문제
- 스크롤을 내리면 컨텐츠들이 계속해서 쌓입니다.
- 너무 많은 컨텐츠가 쌓이면 렌더링 비용이 너무 비쌌습니다.
해결
- 화면에 보이는 컨텐츠만 렌더링하면 아무리 컨텐츠가 많아도 성능이 떨어지지 않을 것이라 생각했습니다.
- 목록 가상화를 통해 화면에 보이는 컨텐츠만 렌더링 되도록 도와주는 React virtualized 패키지를 도입했습니다.
- 화면에 보이는 컨텐츠만 렌더링 돼서 스크롤을 내리더라도 새로운 컨텐츠를 추가하는 렌더링 비용이 비싸지지 않았습니다.
문제
- 무한 스크롤에서 스크롤을 아래로 내려 새로운 컨텐츠를 렌더링하고, 다른 페이지로 이동했다가 뒤로가기 하면 스크롤은 유지 되지만 해당 위치에 데이터가 없어 사용자는 빈 화면을 봤습니다.
해결
- React query가 해당 위치에서 필요한 데이터를 Caching 해서 재사용해왔고, 스크롤 복원을 해도 빈 화면을 보지 않았습니다.
문제
- Next.js의 서버사이드 데이터 페칭 함수인
getServerSideProps
를 사용하면 스크롤 위치가 최상단으로 이동하는 이슈가 있습니다.
해결
- 페이지를 떠나기 전에 현재 경로와 스크롤 위치를 세션 스토리지에 저장합니다. 그리고 뒤로가기를 통해 그 페이지에 다시 방문하면 세션스토리지에 있던 스크롤 위치로 이동시켜줬습니다.
문제
- React virtualized를 사용하면
window.scrollTo()
가 정상적으로 작동되지 않는 것을 확인했습니다.
고민
- 해당 이슈는 아직 해결하지 못했지만, React virtualized 모듈에서 스크롤의 높이를 계산하는 과정이 여러번에 걸쳐 비동기적으로 이루어져
window.scrollTo()
함수와 충돌나는 것이라 예상하고 있습니다.