안녕하세요, 아티투에서 프론트엔드 개발을 담당했던 민요한입니다.

저는 비전공자입니다.

제목으로 어그로를 끌었는데, 사실 허수도 많았을 것이고 운이 좋았다고 생각합니다.


입사 당시 아티투의 팀메이트들은 저를 제외한 전원 서울대 출신 또는 재학중인 상태였습니다.

합격 후에 면접을 봤던 팀메이트들이 좋은 점수를 주었던 이유들을 포함하여 스타트업 생활을 하며 보고 겪은 것들에 대해 회고하고자 합니다.

회사가 망했어요.

첫 회사였던 쏘쏘마켓은 '이웃스토리'라는 제주 지역 기반의 커뮤니티를 서비스하는 스타트업이었습니다.
저는 새롭게 추가될 유료 구독 관련(지금 생각하면 SaaS형태의)서비스의 페이지 및 기능을 담당했습니다.

(리액트 네이티브는 덤)
여러 악재들이 겹쳐 결국 입사 4개월 만인 23년 10월 30일에 팀이 해체되었습니다.

 

왜 아티투?

그리고 많은 일이 있었습니다. 서류 착오로 면접 하루전에 면접 기회가 박탈되거나,

서류 합격 이후 연락이 없어 연락을 드렸더니 내부 사정으로 채용 취소가 되었다거나 하는 일들을 겪으면서 자존감이 조금 낮아졌었습니다.
지금에야, '더 좋은 곳에서 더 성장해서 더 멋진 개발을 해야지, 더 좋은 일이 있으려고 나쁜 일이 있나보다'라고 생각하지만

개인적으로 쉽지 않은 시간이었던 것 같습니다.

제가 회사를 선택하는 기준은 지금이나 그 때나 같습니다.


1. 실제로 이용자가 있는 서비스가 운영되고 있는가
2. 내가 얼만큼 성장할 수 있는가
3. 나는 얼만큼 기여할 수 있는가
4. 기여한 만큼 대우 받을 수 있는가

그러던 중에 발견한 아티투 공고에는 다음과 같이 적혀있었습니다.
'팀메이트 전원 서울대 출신', '실제 서비스 중', '토스 출신 개발자가 작성한 소스코드 열람 가능 및 멘토링'
우대 사항 일부 (Amplitude 사용 경험)를 제외하고 제가 기여할 수 있는 포인트들과 어떻게 성장할 것 인지를 어필했습니다.

우리나라 최고 대학 출신들은 어떻게 생각하는지, 어떻게 결정을 내리는지, 어떤 태도나 습관을 가지는지가 궁금하다고 솔직하게 적었습니다. 

 

첫인상 - 면접.. 망한 것 같은데?

서류 합격 이후에도 솔직히 나름 전략적으로 행동했습니다.

면접 가능 일의 첫 날을 골랐습니다. 임팩트가 없으면 잊혀질 수 있지만, 반대로 임팩트가 있다면 끝까지 기억날 것이라고 생각했습니다.

제가 첫 면접자라면, 저를 기준으로 다른 지원자들을 판단할 것이라고 생각했습니다.

식사 시간 바로 다음 시간을 골랐습니다.
너무 이른 아침은 저도 제대로 머리를 굴릴 수 없을 것 같고,
면접을 보는 사람도 개인 업무를 미뤄야할 것 같다고 생각했습니다.
또 다른 이유로는 밥을 먹고 나면, 사소한 실수에 너그러워 질 것 같았습니다.


아티투의 면접은, 약 2시간 조금 넘게 진행되었고 기술 면접 - 컬처 핏 면접 순으로 진행됐습니다.
정말로 모두가 데자와를 마시고 있는 모습에 신기했습니다.
인상깊었던 포인트가 좀 많은데 CEO인 덴지의 말이 기억에 남습니다.
"다 똑같다. 서울대생 별 거 없다는 것을 아는 것만으로도 달라지는 것이 있지 않을까요"
(나중에 알게 된 사실 - 졸업장에 3개의 학과가 적힌 사람이...)

저는 사실 면접이 망했다고 생각했었습니다.

당시에 너무 오랜만에 개발이야기를 그것도 고민하던 주제를 현업 개발자와 이야기할 수 있다는 사실에 신이 나서 너무 딥하게 이야기했다는 점
(A라는 것을 물었는데, A를 포함한 A- ~ AA까지 떠들었었어요..)
이 때문에 간단명료하게 핵심을 전달하지 못했다는 점,
면접관이었던 한 팀메이트는 질문이 별로 없었던 점 등 때문이었습니다.

자존감이 조금 낮았던 때라서 더 그렇게 생각했던 것 같기도 합니다.

약 일 주일 쯤 후 송신된 오퍼레터를 받고, 시간이 흘러 팀메이트들과 첫 회식에서 합격 소감이나, 경쟁률, 왜 저를 뽑았는지 등에 대해서 자세히 들어볼 수 있었습니다.

1. 기술을 선택하거나 사용할 때 단순 사용한 것이 아니라 이유가 있는 것이 좋았다. - BE 담당 팀메이트

2. 기술 면접 때 기술 발전의 히스토리나 역사를 이야기하는 것이 인상깊었다. 본인도 좋아한다. - BE 담당 팀메이트
3. 상기 이유 들에서 '덕후' 냄새가 났다. - COO
4. 지원서에서 아티투에 바라는 점, 본인이 부족한 점, 부족한 점을 어떻게 극복하고 기여할 것인지 잘 묻어나서 좋았다 - 모든 팀메이트


더 많은 이유들을 들었었지만 기억나는 것은 이정도입니다.

기술 면접 직후 약간의 쉬는 시간 때 COO인 겐도와 사담을 나눌 때 기술 면접 난이도에 대해서 이야기 했었는데,

장난처럼 '더 어렵고 딥해도 좋을 것 같다'는 의견을 남겼었던 것이 실제로 면접 담당이었던 개발자 팀메이트들에게도 공유됐던 것을 알게되어 당황했던 것도 기억에 남네요.

장난처럼 '아 그거, 사실 어려웠는데 뒤에 보시는 분들 더 어렵게 보시라고 그렇게 이야기 해봤어요 :)' 라면서 넘기긴 했지만요.

살아남기 시작, 마음가짐?

"날 선택해 줄 줄은 정말 몰랐어.." 상태에서 합격 이후 계약서 작성 때까지 개인적으로는 고민이 조금 많았습니다.
계약서 서명 이후 당시 비교 대상이었던 타 회사에 비해 연봉이 적은 점을 솔직히 오픈했습니다. (약 천만원 차이났거든요)
그리고, 서비스의 비전에 대해 물었습니다.

단순하게 담당 업무(개발)을 제외하더라도 만든 서비스가 더 사랑받았으면 좋겠고, 오래 유지됐으면 좋겠다고 생각했습니다.

어떻게 성장할 생각이고, 당장 어떤 것을 더 발전시킬 것이고, 현실적인 문제(투자 or 유료화 등등)에 묻고, 대답을 들으면서 선택에 확신이 생겼습니다.

 

지금이나 이 때나 연봉에 대한 제 생각은 같습니다.

받은 것보다 더 하면, 누군가는 가치를 알아줄 것이고 그러다보면 올라갈 것이라고요.

3개월 종료 시점에서 연봉이 올랐고, 거기서 2개월이 지난 시점에서 차이났던 연봉보다 더 큰 스톡 옵션을 받았습니다.
운이 좋았다고 생각합니다.

 

아티투의 문화, 팀원으로의 성장

아티투에는 매 주 회의에서 스프린트 진행 상황에 대해 다 같이 이야기하는 문화와 스프린트 종료 후 다 같이 회고하며 KPT를 공유하는 문화,

그리고 한 달에 한 번 대표인 덴지와 1on1으로 아티투와 팀원이 서로 바라는 것을 공유하는 문화가 있습니다.

스프린트 관련 회의 때는 서비스 성장 지표 등 팀메이트로서 알면 좋은 정보들이 공유되어 좋았고,
개발자로서는 개발 일정을 공유하고, 이 과정에서 리소스 분배와 팀에게 더 이로운 결정을 내리는 방법을 배울 수 있어 좋았습니다.

비즈니스 입장에서 놓치지 말아야할 것과 포기해야할 것,

개발자의 입장에서 놓치지 말아야할 것과 포기해야할 것 들을 고민하는 시간들을 통해 성장할 수 있었다고 생각합니다.

1on1 때는 저는 깨닫지 못하는 제 모습들을 개선할 수 있어서 좋았습니다.
예를 들어, 내 시간(업무 시간 제외)를 활용하면, 앞서 적은 포기해야할 것을 가져갈 수 있다면 시간을 더 사용하는 것이 옳다고 생각하는 저의 입장과

반 년 뒤, 일 년 뒤의 제가 혹시 지쳐쓰러지면 그 부담이 팀원들에게 전파될 수 있다.

그렇기에 수면 시간을 늘리기를 바란다는 덴지의 입장 같은 것들이 있었습니다.

당시 제 스케줄은 아래와 같습니다.

 

06:00~06:30 | 기상

07:00 ~ 08:30 또는 09:00 | 아침 운동
09:30 | 버스 탑승
11:00 | 근무 시작
20:00 | 근무 종료
20:50 | 버스 탑승
21:30 | 집 도착
22:00 ~ 02:00 또는 03:00 | 개인 활동(개인 공부 같은)


출퇴근 이동 간에는 보통 당시에 해결하고 있던 문제들의 방향성이나 관련 자료를 찾으며 보냈고,

고민으로 문제가 해결되지 않으면 집에와서 관련 자료들을 찾거나 코드를 수정하곤 했습니다.

이 후에는 비슷했지만, 하루에 보통 5시간~6시간은 잘 수 있도록 했던 것 같습니다.

덴지의 배려로 근무 시간을 일부 변경해서 8시간 자는 날도 있었고요!

 

개발자로서의 성장

아티투는 새로운 기능도 많지만, 있던 기능이 변경되는 경우도 많았습니다.
그리고 기존의 서비스가 가지고 있던 고질적인 문제들도 당연히 존재합니다.
또, 사용 유저분들의 불편 사항에 최대한 즉각 대응하는 편이기도 합니다.

선배님들이나, 또는 누군가가 보기에는 별 거 없을지도 모르지만, 많은 경험을 한 것 같습니다.

라이브러리 이슈인지 모르고 삽질하다가, 라이브러리의 깃 헙의 이슈 리포트를 통해 버전 이슈라는 것을 알고 버전을 확인했더니 문제가 발생하고 있는 버전이어서 간단히 해결되어 허탈함과 안도감을 느낀 적도 있고,

실제로는 제대로 동작되고 있지 않았으나, 여러 원인들 때문에 지금껏 잘 동작하는 것처럼 보였던 것을 발견하여 처음부터 뜯어 고쳐야했던 적도 있었습니다.

안드로이드에선 지원하지만, iOS에서는 지원하지 않는 경우도 그 반대의 경우도 있어 새로운 방법을 찾아야하는 경우도 있었습니다.

테스트 환경에서는 너무 잘 동작하다가 라이브 서버에서는 동작하지 않는 아찔한 경우도 있었습니다.

유저의 '안 된다.'라는 한 마디 힌트밖에 얻지 못하는 경우가 많아, 여러 가지 가설을 검증하고 부딪히며 많이 배운 것 같기도 합니다.

개발자로서 해본 것 보다 해보지 않은 것들이 많아 즐거웠고, 배울 것들이 아직 많이 남아있어 설레고 즐겁습니다.

 

좋은 서비스, 그렇지 못한 비즈니스

좋은 문화와 좋은 팀원들, 그리고 성장하고 있는 서비스이고 투자금도 많이 남았습니다.

그럼에도, 아티투의 팀 곧 해체합니다.

저도 이제 다시 새로운 팀을 찾아야하는 입장이 되었습니다.


영화나 미국 드라마에서만 볼 것 같은 스타트업이었고, 개인적으로 너무 만족하는 회사 생활이었습니다.
많이 배웠고, 함께 할 수 있어서 성장할 수 있었습니다.


정말 아쉽지만, 
'앞으로 얼마나 더 멋진 팀원을 만나, 더 멋진 개발을 하고 성장할 기회가 생기려고 이러지?'하고 생각하려고 합니다.
마주했던 문제들의 디테일한 내용은 차차 블로그에 포스팅할 예정입니다.

긴 글 읽어주셔서 감사합니다.
읽어주신 분들도, 행복한 하루가, 내일이 되셨으면 좋겠습니다.

도입하게된 계기

현재의 직장의 소셜로그인은 [구글, 카카오톡, 애플]을 사용하고 있었다.

웹 서비스와 웹뷰를 사용한 앱을 서비스하고 있는데 아래와 같은 문제가 있었다.

1. iOS의 문제
P: 카카오톡이 깔려있지만, 이메일과 비밀번호를 입력하는 입력 창이 앱에서 새창 Safari로 열림 -> 로그인 -> 앱으로 돌아오지 못함 -> 돌아 오더라도 앱에서 로그인처리가 되어있지 않은 경우가 있음

P2: 1차 로그인 이후 2차 로그인 때부터 새창에서 앱으로 돌아오지 못함

2. AOS의 문제

P: 크롬 창이 열림 -> 이메일과 비밀번호를 입력하는 창이 뜸 -> 로그인 -> 앱으로 돌아오면 로그인처리가 되어있지 않음


3. CS
P: 특정 기능 업데이트 이후 전체적인 유저들의 로그인이 풀리는 사태가 발생했다. 복합적인 원인이 있었지만, 이 이슈로 인해 앱을 사용하여 소셜로그인을 이용하는 유저들의 CS가 폭증했다. 앞서 서술했던 앱으로 돌아오지 못하는 문제와 로그인처리가 제대로 되지 않는 문제와 맞물려, 아이디 | 비밀번호를 입력하는 과정을 몇 번씩 반복하는 과정에서 신뢰도와 만족도가 떨어지는 것이 체감될 정도였다. (추후 알게된 사실인데, 이 과정에서 평소보다 로그인 시도가 평소보다 40배 정도 증가했었다.)

-> 바로 대응할 수 없는 부분이 있어, 소셜로그인이 아니라 서비스의 아이디와 비밀번호로 로그인하는 기능을 차선으로 안내했지만, 이 부분에서 큰 자극을 받았다. 왜 해결하지 못하지?

4. 팀원들의 이야기

팀원 분들의 '다른 앱처럼 카톡이 깔려있으면, 카카오톡이 열렸으면 좋겠어요',  '아이디와 비밀번호를 입력해야하는게 너무 번거로워요', '어디서 봤는데 이렇게 아이디 비밀번호 입력하는게 최악의 서비스래요. 우리 서비스 최악인가요?'등을 들으며 결심했다.
이거 꼭 해결해야겠다.

 

원인을 파악해보기

Q: 왜 Safari가 새로 열릴까? 로그인 과정이 어떻게 되는거지?
-> 카카오 로그인은 사이드 프로젝트에서도 도입해본 적이 없어서, 정보가 거의 없는 상태였다.

-> 원인을 파악하기 위해 앱 코드를 열었다. 회사 코드기에 구조를 상세하게 서술할 수는 없지만 처음보았을때는 문제가 없는 줄 알았다.
-> 카카오 디벨로퍼 사이트에서 iOS의 로그인 문제를 검색해서 글들을 읽기 시작했다.
-> '간편 로그인'이라는 키워드를 얻었고, 카카오 로그인에서 'REST API 방식' | 앱 SDK | 'JS SDK' 방식을 제공하고 있다는 것을 알았다.
-> 공식 문서를 확인하며, 우리가 원하는 것이 간편로그인이며
리액트 네이티브를 이용한 하이브리드 앱을 사용하고 있는 점, 웹뷰를 사용하는 점 등에 따라 원하는 동작을 구현하려면 간편 로그인을 사용해야 한다는 것을 알았다. 
-> 웹의 로그인 쪽 코드를 보고, 통해우리가 'REST API' 방식을 사용하고 있다는 것을 알았다.
-> 여러 글을 확인하고, JS SDK방식으로 변경해야함을 깨달았다.

-> user-agent를 파악해서, 모바일 환경인지 그렇지 않은지에 따라 js SDK | rest API를 사용할 수 있도록 코드를 구현했다.

-> 빠르게 테스트 코드를 작성하고, iOS에서 트라이했지만, 로그인 처리는 되는데 사파리가 새롭게 열렸다.
-> 웹뷰 코드를 다시 열었다. user-agent에 대한 처리, 스킴 처리나 url이 어떻게 되는지 파악하고 싶었다.
-> talk-apps로 시작하는 url에 대한 처리가 없어 사파리를 통해 여는 코드로 빠지고 있음을 알았다.
(해당 url은 이전에 웹에서 카카오의 PF(플러스 친구)에서 앱을 여는 과정의 트러블 슈팅을 통해 앱실행을 위한 유니버셜 링크임을 알고있었다.)
-> 소셜 로그인을 처리하는 쪽에 인 앱에서 처리할 수 있도록 처리하였다.

Q:다른 대안은 없었나?
Q1. 앱 SDK는 왜 사용하지 않았나?

-> 서비스 특성 상 자세히 기술할 수는 없지만, 앱 SDK를 사용할 수 없는 이유가 있었다.
Q2. @react-native-seoul/kakao-login(https://github.com/crossplatformkorea/react-native-kakao-login) 립을 통해 해결하는 것은?
-> rn을 사용하면, 립을 사용할 수 있다는 것을 검색을 통해 알았지만, 마찬가지로 앱SDK를 사용해야하기 때문에 사용하지 않았다.

Q: AOS? 웹뷰?
-> 여차저차 iOS를 해결하고, AOS로 넘어와서 테스트를 진행했다. 에러가 발생했다.
-> 여기저기 검색해서 문서 | 글을 읽었다.
-> agent에 wv가 있어서 문제라는 의견도, 이를 처리하기 위해서 웹 - 웹앱 간에 메세징 방식을 택하거나, React-native-webview를 포크해서 특정 부분을 고치는 방법이나, 위에 서술한 라이브러리를 사용하는 방법, wv를 제거하는  등, 원인 파악보다는 해결 법에 대한 정보들이 많았다.
-> 'intent'에 대한 정보를 습득했다. intent에 대한 처리가 없음이 원인임을 알았다.
-> RN 폴더 안에 android에 속한 코드를 보기 시작했다. AndroidManifest의 Activity를 보기 시작했다. 이해할 수 없어, 더 안쪽에 MainActivity와 MainAplication 코드를 읽기 시작했다. 코틀린이라 명확하게 이해할 수 없어 다시 검색을 시작했다. 
-> intent처리를 위한 모듈을 작성하고, 패키지로 묶어 MainAplication에 추가해야한다는 것을 알았다.
-> g선생님(Chat GPT)을 통해, 모듈을 작성하고 패키지로 묶어 추가했다. 다른 에러가 발생했다. 스킴을 제대로 처리하지 못하는 문제였다.
-> 웹뷰 코드로 돌아와 whiteList를 통해 인텐트 스킴을 받을 수 있도록 처리해주었다.
-> 로그인 처리가 되었다. 감격스러웠다.

Q: 구글로그인은 갑자기 왜?
-> 문제를 해결하고 테스트를 진행하던 중에 AOS쪽에서 구글 로그인이 제대로 동작하지 않았다. 크롬이 열리고, 흰화면이 보이는 현상이었다.
-> intent를 처리하며 넣은 모듈이 문제인가 싶어 모듈이나 패키지를 다시 확인했지만, 이쪽 문제는 아닌 것 같았다.
-> 다시 검색을 시작하며, intent-filter가 androidManifest에 제대로 작성되어 있지 않음을 알았다.
-> Action과 category를 분류하여 intent-filter를 나누고, 스킴에 대한 부분도 다시 작성하였다.
-> 문제가 해결되었다. 

 

왜 글을 쓰게 되었나?

보통은 처음 나오는 이 부분을 마지막에 적은 이유는, 내 접근 방법이 나와 비슷한 환경에서 고통받는 누군가에게 조금의 도움이라도 되길 바라기때문이다.

검색 실력이 부족해서 오래 걸렸고, 다른 더 빠른 방법이 분명 존재하며, 내부코드라 코드도 첨부할 수 없지만, 서비스마다 다른 환경에서 라이브러리 조차 선택할 수 없는 상황에서 카카오 로그인을 도입하는 누군가에게 도움이 됐으면 좋겠다.

문제 목차

1. 드래그 종료시 새로운 맵 생성해서 Element쌓임
1-1. 드래그 종료마다 재 렌더링

2. 화면 이동 -> 지도로 돌아왔을 때 이전에 클릭했던 마커를 기억함

3. 이용자 위치 수정 시 제대로 렌더링 하지 않음.

4. 마커 중복 렌더링

5. 이용자 위치 권한 거절 시 렌더링 하지 않음

6. Update 이후에 Bounds를 업데이트 하지 않음

7. 6번으로 인한 Marker 손실과 재렌더링

8. 6, 7번으로 인한 위치 변경 이후에도 이전 마커 기억하는 문제

9. 모드 변경 이후에 마커를 새로 그리지 않고, 기존 위치로 이동 시 마커 새로 그리지 않는 문제

10. update변경으로 이용자가 설정 변경하다가 창 종료하면 그 지점의 지도를 기억하는 문제

 

원인 및 해결

1. Drag 종료 시 새롭게 맵 생성 -> Update하는 방향으로 변경

해결 및 대응: 그러나 Update한다는 전제가, 이전에 커스텀(지도 중앙) Control이나 마커를 기억한다는 것이기때문에 물리적으로 Element 생성 요소를 제거함

 

Destroy하지 않은 이유?

-> Element는 깔끔하게 관리되나, 의존성 문제로 재생성 마다 재렌더링되기 때문에 지도 내부 요소들이 전부 깜박거리는 것처럼 보임

 

현 시점의 의견?

->component를 좀 분리하여 렌더링 의존성을 줄일 것 같음. 트리거 방식을 활용하는 것도 더 좋아보임.

2. 게시글 이동 -> 지도로 돌아왔을때 이전에 클릭했던 마커 기억

원인: 마커 생성 시점에서 판단 기준이 되는 상태 요소 중 DOM Reference객체를 잃어버렸기때문에, 관련 요소를 삭제해야하는데 제대로 삭제되지 않음.

 

해결: Atom 업데이트 시점의 상태가 명확하지 않아서였기때문에 useEffect로 관련 상태 초기화하는 로직 추가함.

3&4 이용자 위치 수정 시 제대로 렌더링 하지 않는 문제와 마커 중복 렌더링

원인: 컨텐츠 마커의 경우 드래그 종료 시점마다 map의 영역 변경이 필요, 의존성이 있는 상태 변경마다 계속 업데이트하기때문

 

해결: 중복렌더링 가능성이 있는 요소들 제거 후 Trigger 통해서 최소한으로 로딩되도록 변경

5. 이용자 위치 권한 거절 시 렌더링하지 않는 문제

원인: 브라우저 또는 컴퓨터 설정 상 location 거절하는 경우나 유저가 직접 거절하는 경우 getCurrentPosition method가 동작하지 않기때문

 

해결: 이전 회의때 나왔던 의견처럼 initialPosition을 제주도청 좌표로 변경하고, 유저 지역 상태 변경 후 API요청하여 백엔드에서도 현 유저 상태로 업데이트할 수 있도록 함 -> alert으로 새로고침 이후로도 제대로 업데이트 되지 않는 문제 발생하여 Async/Await으로 로직 동기화, Trigger에 관련 로직 추가하여 Map객체 생성할 수 있도록 변경.

6. Update 이후에 Bounds(좌표 범위) 업데이트 하지 않는 문제

원인: 맵을 Create할때만 Bounds관련 상태 변경하도록 로직처리 되어있었음.

 

해결: Dragend시에도 상태 업데이트 하도록 로직 추가

7. 6번으로 인한 Marker 손실과 재렌더링

원인: 기존에 Marker 관련 변수가 컴포넌트 지역 변수로 선언되어 있어, 지도 Update시점의 Marker는 Undefined, 따라서 업데이트나 생성 시 참조하는 변수가 초기값 또는 Undefined였음.

 

해결:  useState로 지역상태로 기억하도록 변경하고, useEffect통해 Marker 생성 시마다 재렌더링(MarkerUpdate)하여 맵에 생성된 마커 추가할 수 있도록 변경함.

8. 6번, 7번으로 인한 위치 변경 이후에도 이전 마커를 기억하는 문제

원인: 이전에 생성했던 마커의 ID를 Key로 삼아 마커 재생성을 하기때문에 이용자의 위치가 변경되어도 마커를 새로 생성하거나 변경하지 않음.

 

해결: useEffect 의존성 변경과 아래의 로직 추가

1안 -> 모드 변경 시 기존의 마커를 clear해주는 방안

2안 -> map 관련 method(생성이나 업데이트) 사용 중, .every method 활용하여 결과값이 false라면, 안쪽에서 분기문 설정.

Position 값이 변경되면, Marker의 Position도 변경해주는 방안

 

선택: 1안

이유: Drag Event가 일어나 Position변경 -> 생성 -> Every 결과값 확인 하는 것이 느린 상황에서, 마커의 배열에서 찾고, 포지션을 다시 변경해주고 하는 것이 옳지 못하다고 느꼈음.
당시 한 번에 그리는 마커 갯수가 10000개 미만이었기때문에 갯수 총량을 10000개나 10만개라고 본다면,

1안 최대 10만(기존) * 10만(생성)

2안 최대 10만(기존) * 10만(생성) * 10만(변경)의 차이가 있을 것 같아 좀 더 명확하게 업데이트 할 수 있는 로직으로 Refactoring해야한다고 적혀있음.

9. 모드 변경 이후에 마커를 새로 그리지 않고, 기존 위치로 이동 시 마커 새로 그리지 않는 문제

원인: 모드 변경때 마커를 지도에서 제거한 것이지, 실제 객체는 남아있어 이용자 위치가 변경되면 마커 위치가 변경되어야하나, 위치 변경시마다 렌더링하거나 모드 변경시마다 재렌더링 할 수 없는 문제. 명확히는 상태 관리 문제

해결: 마찬가지로 useEffect의 의존성 변경과 업데이트 이후 .refresh method(Map에 있는)활용하여 모드 변경마다 지도 내용 새로고침해줄 수 있도록 변경.

10. Update 변경으로, 이용자가 설정 변경하다가 창 종료하면 그 지점의 지도로 위치를 기억하는 문제

원인: localStorage에 저장된 user의 초기 Position 기준으로 맵을 새로그리게 되는데, 브라우저 종료되더라도, position정보는 손실되지 않기때문에 일어난 일

 

해결:
1안-> 저장 장소를 sessionStorage로 변경

2안-> 초기 렌더링 시 position 초기화

 

선택: 2안

이유: 1번으로 선택할 경우 탭 닫았다가 다시 접근하는 경우 의도대로 동작하지만, 일반 새로고침으로 돌아오는 경우 마지막 지도의 위치를 기억하고 있음. 초기 렌더 초기화해주는 로직이 필요함.

 

만약 1안으로 한다면?
interface 생성해서 storage에서 가져오는 데이터 타입 맞춰주고, sessionStorage로 AtomStorage의 메서드와 타입 변경해줘야한다. 추가로, 메인에서 초기렌더링 시 동작하는 useEffect에서 한번 초기화 해주는 로직이 필요함.

서론

최근, 사이드 프로젝트(클론)을 진행하며 했던 고민을 정리해두어야할 것 같아서 적게되었다.
기억을 더듬어 사고의 흐름대로 한번 따라가보려고 한다.

고민의 대상

고민의 시작은 드롭다운의 컨텐츠 관련 컴포넌트였다.

내 고민의
주범이 되었던
코드 조각들

이전에 Refactor쪽에서 다뤘듯, 언뜻 보면 별로 문제가 없어보인다.

근데, 내 주관적인 시점에서는 더 좋아질 수 있을 것 같다고 생각했다.

1. 고민의 시작점: Route 정의가 반복되는 것 같은데, 문제가 있지 않나? 나중에 갯수가 많아진다면?

2. 그렇다면 import가 이 컴포넌트에서 될 필요가 있는가?(관리 책임이 여기에 있나?)
3. 이 컴포넌트에서 맵을 돌려서 렌더링을 해야하는 이유, 또는 정의를 해야하는 이유가 있나?

 

A.1 잘 하면, 공용으로 뺄 것들을 분리할 수 있을 것 같은데?

문제 1번을 바라보면서 고민했다.

"아, 원하는 url + / + id + / + label 꼴인데, url, id, label을 바깥에서 받아서 그냥 리턴하는 함수를 만들자"

-> 저형태로 반복되는 url을 만들어낼때 편하겠다 ->근데 label과 endpoint가 다르면 어떡하지? -> 일단 보류

 

"흠, label이랑 JSX이름이 같네, label을 집어 넣으면 JSX를 리턴하는 function을 만들자 -> label이랑 JSX이름이 다르면 어떡하지?

->

A.1-1번, 배열로 JSX들을 들고 있는다 -> 해당 function call할때마다 전체 배열(지금은 4개지만 추후에 몇개가 될지 모를)다시 만들거나, 전역으로 관리해야함 -> 기각

 

A.1-2번, 어떻게 이름을 넣으면 동적으로 importing해서 JSX를 꺼내오는 방법은 없나? -> lucide-react를 열어보자

이런 형태이고,
이렇게 꺼내지는데..

-> 일단 동적 import 안됨, 방법이 있을지 모르나 내 지식선에서 해결 불가 -> 꺼내지게 하려면, 저 선언들 중 사용할 것들을 다 꺼내놓고name + Icon형태로 받아서 찾아쓸 수 있게 해야함 -> 일이 커짐 -> 기각

 

A.1-3 ifelse? 좀.. 찝찝, "요구 사항을 정리하면 label을 집어넣으면, label에 맞는 JSX를 리턴했으면 좋겠다."니까 Switch를 써보자.

className 중복도 거슬리지만, 불확실한 컴포넌트를 props로 던져서 CSS를 다시 입히는 행위를 할 최선의 방법이 생각나지 않았다.

 

이제 이걸 반복시키면 되는데, 어떻게 할까

label을 string으로 받아서 split하거나, 그냥 string[]로 받는 방식이 생각났다.

후자로 선택한 이유는, 쪼개고 집어넣고, 쪼개고 집어넣고 할때 드는 비용보다 n개짜리 배열을 통으로 집어넣는게 수가 커질수록 더 나을 것 같다고 생각이 들었기때문이다. 맵을 사용하는게 좀 찝찝하긴 하지만, 결과적으로 {}[]를 리턴해야하는 건 맞으니까 괜찮다고 생각했다.

 

보류한 url로 돌아와서, 현 시점에서 특이한 사항은 board의 경우 아무것도 붙지 않는다는 것이고 예측되는 특이사항은 앞서 말했듯 label과 endpoint가 다를 경우이다. 그리고 고려사항은 label을 lowcase로 변환해야한다는 것이다.

 

따라서, suffix(endpoint)를 받았을때, 
Board면 ? endpoint없이 `${url}/${id}` : `${url}/${id}/${toLowCase()`
여기에 추후 다를 경우 추가되면 switch해서 변환한 것을 꺼내오거나 변환하는 과정을 추가하자고 결정했다.

현시점에서는 board일 확률 < board가 아닐 확률이니  

return suffix !== "Board" ? `${url}/${id}/${suffix.toLowerCase()}` : `${url}/${id}`

로 처리함.

A.2,3 관리 책임?

내가 생각했을때 해당 컴포넌트의 주 역할은 컨텐츠인 item을 렌더링하는 것이다.
routes같은 경우 여기서 그려야할 컨텐츠이기때문에 여기서 들고 아래로 내려주는 것이 맞다고 생각했다.
정의나 맵 함수 사용 등은 컨텐츠를 렌더링하는 veiwer역할을 하는 컴포넌트에서 해야한다고 생각했다.
깊이가 깊어지는 것에 대해 고민했지만, 정의에 대한 책임을 분리하는 것이 더 이득이라고 생각했다.

따라서, 이 컴포넌트에서는 위 내용을 정의하지 않는 것이 좋고, lucide-React의 icon들도, useRouter, usePathname, AccordionContent and Trigger등도 존재하지 않아도 된다고 생각했다.

 

결과물

1. isActive,Expanded, onExpand, organization 등은 드롭박스보다 더 위인 organization에 존재해야함으로 props로 받는 것이 타당.

2. Routes의 요소는 navitem의 컨텐츠이기때문이 해당 컴포넌트 안에 존재하고, props로 내려주는 것이 타당.

 

3. Trigger, Contents, Item에 필요한 요소들은 props로 전달하고 세부 정의는 아래 컴포넌트에서 하는 것이 타당. 

4. route를 브라우저 히스토리에 push하거나, pathname을 사용하는 것들은 Contents안에서만 하는 것이기때문에 Contents로 이동

5. Trigger에서 필요한 것들도 Trigger에서만 사용되도록 이동

 

끝!

 

Docs Update가 안되고있다..


Shadcn의 docs 컴포넌트에서 yarn cli를 긁으면, npx명령어로 CLI Command가 저장된다.

Yarn을 사용하면서 npx키워드를 사용하는게 좀 뭔가 찝찝해서 깃허브를 찾기 시작했다.

Shadcn Github 이슈나 PR쪽 확인 해보니, yarn의 1.x.x버전에서도 npx는 당연히 동작한다.

동치어가 없다. dlx를 지원하지 않는다. dlx는 2.x부터 지원한다. 등 여러 의견이 있는 것을 확인함.

 

yarn과 npx 관련 글들도 이전에는 동치어가 없다, 20년도즘엔 dlx가 있다.

yarn 공홈에서도 2.x에서 "npx쓰지마세요, yarn dlx 만들었어요!!" 라는데.. 흠 dlx가 안되는걸까?

 

-> 더 찾아보니 문제없이 사용하시는 분들도 계시고, 나또한 실제로 사용은 가능 하고, yarnpkg shadcn docs에서도 해당 커맨드를 적어놓은 것을 보면 음 아직 잘 모르는 부분이 있는 것 같다만, 계속 해서 공식 깃에 이슈가 올라오는 것을 보면 충분히 헷갈려하거나, 찝찝해하는

나같은 사람이 있는가보다. (npx말고 yarn껄로 해줘!라는 댓글도 있었음)

 

관련해서 이슈를 읽으면, shadcn pro쪽에서 승인을 받아야해서 승인을 넘기는데 블락당하거나 중복된 내용이라고

close되는 것을 보면 수정하려고 하는 것 같은데, 아직 적용되지 않은 것 같음.

->동일한 커맨드 사용한다고 내버려두기로 결정했다는 글을 확인... 흠흠..

 

아무튼 어디선가, 나처럼 관련 이슈나 커맨드를 찾아 헤매는 분들을 위해

berry 아니어도 dlx 쓸 수 있어요.

 

Components 추가

yarn dlx shadcn-ui@latest add [component]

 

ex)yarn dlx shadcn-ui@latest add sheet

 

 

서론


아래 글에서 언급했던, 동영상 댓글을 보던 중 흥미로운 의견이 있었다.

'모달'과 '모달을 호출하는 버튼'은 동일한 구성 요소로 취급하면 안된다는 의견이었다.

그래서 일까? 글을 작성하는 시간 기준으로 열흘 전, 단일 책임의 원칙에 대한 동영상이 하나 올라왔다.

 

중요한 것은 무서운 슈퍼 컴포넌트를 만들어내지 않게,
보다 작게, 관리하기 쉽게, 재사용할 수 있게 만들어내는 모든 노력들

 

그러면서, 리액트에서의 단일 책임 원칙과 스케일러블하고 매니지블한 리액트 코드베이스를 빌드하는 것이 왜 중요한지에 대해 이야기한다.

 

단일 책임의 원칙에 대해

사실, 원티드챌린지때도 그렇고 가볍게 단일 책임 원칙에 대해 공부하긴 했었다.

그때의 멘토님은, 응집성이라는 개념과 긴밀한 연관성이 있다며 실 사례를 예를 들어 설명해주셨었다.

그리고, 단순한 로직 뿐 아니라 이해관계자와의 적극적인 소통을 통해 설계 단계부터 컴포넌트를 어떻게 관리할지 논의하는 것의 중요성도 함께 이야기했었다.

멘토님이 선택한 방법은 컴포넌트와, 요구사항을 담은 테스트 코드 관련 로직을 하나의 폴더안에 응집될 수 있도록 모아두고 관리하는 것이었다.

 

영상에서는 다음과 같이 말한다.

 

The Single Responsibility Principle states that a component should have one, and only one, reason to change. 

단일 책임 원칙을 따르면, 컴포넌트를 변경해야할 이유하나만 존재해야합니다.

 

아래와 같은 점이 좋거든요.

 

1. Reduce Complexity: Dividing your components into smaller, more focused parts makes it easier to understand and manage your code.

복잡성을 줄인다: 컴포넌트를 작게 집중적인 부분으로 나누면 코드를 이해하기 쉽고, 관리하기 쉽게 만든다. 

 

2. Enhance Reusability: When components are focused on a single task, they can be reused across your application and even across different projects.

재사용성이 높아진다: 컴포넌트가 하나의 업무를 담당하면, 어플리케이션과 다른 프로젝트에서 재사용될 수 있다.

(결합도가 떨어지고 분리되어 있을수록 비슷한 기능을 하는 다른 곳에서 재사용될 수 있다는 의미)

 

3. Facilitate Testing: Smaller components with a single responsibility are much easier to test since you have fewer cases to consider.

테스트가 용이해진다: 단일 책임으로 컴포넌트가 작아질 수록, 고려해야하는 케이스가 줄어 테스트가 쉬워진다.

 

4. Improve Maintainablilty: When bugs arise-and they will-you can pinpoint issues faster in a well-organized codebase that follows SRP.

유지관리성이 향상된다: 단일 책임을 다르는 잘 짜여진 코드베이스는 버그 발생의 지점과 이슈를 빠르게 파악할 수 있게 한다.

 

단일 책임 원칙을 위반하고 있는 원본 코드

import { useQuery } from '@tanstack/react-query';

import { Product } from './types/product';

export default function ProductPage() {
  const {
    data: products,
    isFetching,
    error,
  } = useQuery({
    queryKey: ['products'],
    queryFn: async ()=> {
      const response = await fetch('https://fakestoreapi.com/products');
      const data = await response.json();
      return data as Product[];
    },
  })
  
  return (
    <div>
      <h1>Prodcuts Page</h1>
      {isFetching && <p>Loading...</p>
      {error && <p>Something went wrong...</p>}
      {products && (
        <div>
          {products.map((product) => (
            <div key={product.id}>
              <h2>{product.name}</h2>
              <p>{product.price}</p>
              <div>
                <h3>Seller</h3>
                <p>{product.seller.name}</p>
              </div>
            </div>
          ))}
        </div>
      )}
    </div>
  );
}

 

이전 편과 같이 역시 어디선가 볼 수 있는 기본적인 코드다.

 

무엇이 문제일까?

 

제목에서 알 수 있듯, 페이지 컴포넌트다.

1. 페이지 컴포넌트에서 data를 fetching하고,

2. 에러를 체크해서 에러를 띄우고,

3. 로딩상태를 체크해서 로딩을 띄우고,

4. 프로덕트가 있으면 맵을 돌려 렌더한다.

 

페이지라는 '관점'에서는 언뜻 문제가 없어보일지 모른다.

 

그러나, 각각의 '구현'에 집중해보면 아니다.

 

뭐가 많다.

 

이 컴포넌트의 본질은 컨텐츠를 보여주는 것이다.

다른 컴포넌트나 훅에서 관리되고, 이미 만들어져 있는 [부품]들을 가져와 넣고, 조합해서 사용함으로써

컨텐츠를 보여주는 것에 집중해야한다.

 

(useQuery가 만들어진 부품이긴 하지만, 외부에 요청하고 응답한다는 관점에서 볼 때 한 단계 더 추상화할 수 있고

명령과 구현이라는 관점에서 분리되어야한다.)

 

1번 '프로덕트' 가져오기

프로덕트는 이 페이지에서 그려야할 콘텐츠기때문에 존재해도 괜찮다.

그러나, 쿼리키를 지정하는 책임과 fetch를 사용하는 책임은 없기때문에 분리하는 것이 좋다.

 

//hooks/useFetchProducts.ts

import { useQuery } from "@tanstack/react-query";
import { Product } from "../types/product";

//다른 폴더나 function정의로 빠지는 것이 개인적으론 맞다고 생각하고,
//실제로 업무때는 분리해서 만들어놨음.
//유튜버 분도, api folder나 다른쪽으로 옮길 것을 TODO로 메모함.

const fetchProducts = async (): Promise<Products[]> => {
  const response = await fetch('https://fakestoreapi.com/products');
  const data = await response.json();
  return data as Product[];
};

export const useFetchProducts = ()=> {
  return useQuery({
    queryKey: ['products'],
    queryFn: fetchProducts,
  });
};

 

2번과 3번 로딩 | 에러 렌더링

에러와 로딩에따라서 뭔가를 렌더링하는 것의 구현이 페이지에 있는 것보다 더 좋은 방법은 없을까?

//./components/LoadingDisplay.tsx
export default function LoadingDisplay() {
  return <p>Loading...</p>
}

//./components/ErrorDisplay.tsx
export default function ErrorDisplay() {
  return <p>Something Went Wrong...</p>
}

지금은 짧지만, 실제 프로덕트에서는 이것저것 많이 달리게 된다.

로딩만 봐도 스피너나 스켈레톤이 달릴 수 있지 않은가.

 

4번 '프로덕트들' 렌더링하기

//./components/ProductList.tsx

import { Product } from "../types/product";

type ProductListProps = {
  products: Product[];
}

export defualt function ProductList( { products }: ProductListProps) {
  return (
    <div>
      {products.map((product) => (
        <div key={product.id}>
          <h2>{product.name}</h2>
          <p>{product.price}</p>
          <div>
            <h3>Seller</h3>
            <p>{product.seller.name}</p>
          </div>
        </div>
      ))}
    </div>
  )
}

 

결과물과 느낀점


import { useFetchProducts } from './hooks/useFetchProducts';
import { LoadingDisplay } from './components/LoadingDisplay';
import { ErrorDisplay } from './components/ErrorDisplay';
import { ProductList } from './components/ProductList';

export default function ProductPage() {
  const {
    data: products,
    isFetching,
    error,
  } = useFetchProducts();
  
  return (
    <div>
      <h1>Prodcuts Page</h1>
      {isFetching && <LoadingDisplay />}
      {error && <ErrorDisplay />}
      {products && <ProductList products={products}}
    </div>
  );
}

 

사실 굉장히 기초적인 내용이라, 따로 포스팅할 생각은 없었는데 댓글과 덧붙여서 생각을 한 번 더 할 수 있는 기회가 될 것 같아 포스팅을 적기시작했다.

 

개인적인 의견에서는, 영상이 내가 일하면서 했던 고민과 과정과 결과물과 흡사한 방향으로 진행돼서 좀 뿌듯한게 컸다.

 

서론의 댓글에 관해서는? 나도 나누어서 만들었던 편이다.

깊이가 깊어지는 것과 컴포넌트가 비대해지는 것에 대해서 고민을 했던 적이 있었다.

 

나는 댓글처럼 나누는 것이 맞다고 결정했고, 근거는 그때의 로직에서 한 개 또는 두 개 레벨의 깊이가 전체 가독성에서 큰 방해 요소가

아닐 것이라고 판단했다. 그럼에도, 조금 복잡해지거나 할 경우에는 최대한 주석을 달아 바로바로 확인할 수 있도록 했다.

근데 그러다보니 파일이 많아져 관리가 조금 힘들어지거나, 중복되는 요소들이 생겨 공용 컴포넌트나 훅으로 승계하거나하는 과정이 생기기도 했다. 그리고 역할에 집중해서 분리하다보니 경계나 기준에 대해서 고민한 일도 있었다.

네이밍도 좀 문제가 됐었다. 길거나, 비슷하거나할때의 최선을 많이 고민했었다.

 

아무튼, 이런 과정들을 통해 나는 아직 한 번에 좋은 코드를 만들기에는 부족하다는 것을 깨닫고 두 번, 세 번 설계를 돌아보는 버릇이 생겼다.

 

내일은 오늘보다 더 많이 배울 수 있기를

 

[출처]Single Responsibility Principle in React (Design Patterns): https://www.youtube.com/watch?v=tLPi3SPqUSE

가볍고 빠르게 시작


12월에 원티드 챌린지 진행하면서 이전에 내가 작성했던 코드와 고민 그리고 디자인 패턴, 설계들을 얼라인하는 시간을 가졌었다.

한 달이 조금 지난 시점에서 요즘 관심있게 보고있는 'https://www.youtube.com/@cosdensolutions'라는 유튜버분이 마침

기초에 가까운 리팩터링 방식을 소개하는 영상을 보게되어 가볍게 따라가며 다시 정리하려고 작성하기 시작했다.

 

Cosden Solutions

Let's write code to change the world 💻

www.youtube.com

 

원본 코드

import { useEffect, useState } from 'react';
import { useQuery } from '@tanstack/react-query';

import { trackEvent } from '@/utils';

import { fetchUser, fetchUserProducts } from './other';
import ProductsList from './ProductsList';
import UserProductsFilters from './UserProductFilters';


//개인적으로 아래처럼 컴포넌트에 직접 인터페이스 적는걸 선호하지 않는다.

export interface Filters {
	createdAt?: Date;
}

interface UserProductsPageProps {
	userId:number;
}

const UserProductsPage = ({ userId }: UserProductsPageProps) => {
    const [filters, setFilters] = useState<Filters>({
      createAt: undefined,
    });
    
    const [isModalOpen, setIsModalOpen] = useState(false);
    
    const { data: user, isLoading: isLoadingUser } = useQuery({
    	queryKey: ['user', userId],
        queryFn: ()=>fetchUser(userId),
        gcTime: 1000 * 60 * 60 // 1 hour
        staleTime: 1000 * 60 * 60
    })
    
    const { data: products, isLoading: isLoadingProducts } = useQuery({
    	queryKey: ['userProducts', userId, filters],
        queryFn: ()=> fetchUserProducts(userId, filters),
    })
    
    const handleButtonClick = ()=> {
    	setIsModalOpen(!isModalOpen);
    }
    
    // Sends analytics event when the page is loaded
    useEffect(()=> {
    	trackEvent('page_view', { pageId: 'user_products_page', data: { userId } });
    }, [userId]);
    
    const isLoading = isLoadingUser || isLoadingProducts;
    
    if(isLoading) {
    	return <div>Loading...</div>
    }
    
    return (
    	<div className='tutorial'>
          <h1>hello {user!.name}! //'!의 의미는 널이 아님을 컴파일러에게 전달'
       	  <button onClick={handleButtonClick}>
            {isModalOpen ? 'Close Filters' : 'OpenFilters'}
          </button>
          {isModalOpen && <UserProductsFilters onChange={setFilters} />}
            {products && products.length ? (
              <ProductsList products={produts} />
          ) : (
              'No Products available.'
          )}
        </div>
   );
};

export default UserProductsPage;

 

아주 흔하게 볼 수 있는 컴포넌트의 형태이다.

이 컴포넌트는 아래의 것들을 수정하면 조금 더 좋아질 가능성이 있다.

 

1. 컴포넌트 내부에서 useQuery를 직접 사용한다.

2. 모달 관련 지역 상태, 업데이트 함수가 포함되어있으며 이를 통해 JSX를 렌더링한다.

3. 액션이 포함되어있다.

 

고민해봐야할 내용


유튜버분은 다음과 같이 말한다.

당신의 리액트 코드에 지역 상태가 포함되어있다면 스스로에게 물어보세요.
이 상태(코드)가 이 곳에 있어야하는가
아니면 다른 곳(부모, 다른 컴포넌트 등)으로 이동해야하는가

 

정말 많이 했던 고민이라서 반가운 내용이었다.

여러가지 이유나 근거가 있겠지만, 가장 보편적인 이유는 리렌더링이다.

지금의 상황이라면, UserProductsPage는 모달이 열리고 닫힐때마다 컴포넌트가 리렌더링 될 것이다.

 

단일 책임의 원칙을 가볍게 설명하며 '분리하자'고 말한다.

//UserProductsFiltersButton

import { useState } from 'react';

import UserProdutsFilters from "./UserProductsFilters";

interface UserProductsFiltersButtonProps {
}

const userProductsFiltersButton = ({}: UserProductsFiltersButtonProps) => {
    const [isModalOpen, setIsModalOpen] = useState(false);
    
    const handleButtonClick = ()=> {
    	setIsModalOpen(!isModalOpen);
    }
    
    return (
      <button onClick={handleButtonClick}>
        {isModalOpen ? 'Close Filters' : 'OpenFilters'}
      </button>
      {isModalOpen && <UserProductsFilters onChange={setFilters} />}     
    )
}

 

위 코드를 보면 setFilters가 필요하다.

모달 오픈같은 경우는 이 컴포넌트에서 사용되는 것이니까 옮기는 것이 좋다.

반면에 filters는 부모에 유지하는 것이 좋다.

 

1. filters는 fetching을 위해 부모에서 사용된다.

2. fetching을 위한 코드들까지 모달로 옮기는 것은 말이 안 된다.

라는 근거가 있기때문이다.

 

//UserProductsFiltersButton

import { useState } from 'react';

import UserProdutsFilters from "./UserProductsFilters";
import { Filters } from './UserProductsPage';

interface UserProductsFiltersButtonProps {
	onChange: (filters: Filters) => void;
}


const userProductsFiltersButton = ({ onChange }: UserProductsFiltersButtonProps) => {
    const [isModalOpen, setIsModalOpen] = useState(false);
    
    const handleButtonClick = ()=> {
    	setIsModalOpen(!isModalOpen);
    }
    
    return (
      <button onClick={handleButtonClick}>
        {isModalOpen ? 'Close Filters' : 'OpenFilters'}
      </button>
      {isModalOpen && <UserProductsFilters onChange={onChange} />}     
    )
}

부모에서는 해당 컴포넌트를 콜하고, setFilters를 넘겨주기만 하면 된다.

 

두 번째는 useQuery를 사용하는 부분이다.

특정 상황에서 코드 조각들을 반복사용될 수 있는 점 때문에 커스텀 훅으로 옮길 것을 제안한다.

챌린지 때의 내용을 추가하자면, 액션(외부 세계와의 소통)이며 다른 비슷한 라이브러리 또는 대체제로 변경될 때를 위해

'가볍게' 만들어줄 필요가 있다. (다른 말로, 의존 관계를 줄인다)

//useFetchUser

import { useQuery } from '@tanstack/react-query';

import { fetchUser } from './other';

export const useFetchUser = (userId: number) => {
   const query = useQuery({
    	queryKey: ['user', userId],
        queryFn: ()=>fetchUser(userId),
        gcTime: 1000 * 60 * 60 // 1 hour
        staleTime: 1000 * 60 * 60
   })
   
   return query;
};

 

마지막은 useEffect의 관한 부분이다.

프로덕트 관점에서 유지보수를 생각할때 현재의 코드는 더 좋아질 수 있다.

키 값이 변한다던지, 누군가 잘못사용했을때 문제점을 파악하기가 힘들어지는 문제점들을 어떻게 해결해야할까

타입스크립트를 이용하여 더 직관적인 해결책을 이야기한다.

 

//useTrackPageView

import { useEffect } from 'react';

import { trackEvent } from '@/utils';


interface UserProductsPageEvent {
	pageId: "user_product_page";
    data: { userId: number };
}

interface ProductPageEvent {
	pageId: "product_page";
    data: { productId: number };
}

type PageEvent = UserProductsPageEvent | ProductPageEvent;

export const useTrackPageView =(evnet: pageEvent, deps: unknown[]) => {
    // Sends analytics event when the page is loaded
    useEffect(()=> {
    	trackEvent('page_view', event);
    }, [deps]);
}

 

1. interface를 통해 혹시 모를 오탈자를 방지한다.

2. pageEvent가 어떤 것인지 직관적이며, 잘못된 props가 넘어오는 것을 방지할 수 있다.

3. 캡슐화되었다.

 

결과물 & 느낀점


import { useState } from 'react';

import ProductsList from './ProductsList';
import UserProductsFilters from './UserProductFilters';
import UserProductsFiltersButton from './UserProductsFiltersButton';
import useFetchUser from './useFetchUser';
import useFetchProduct from '.useFetchProduct';
import useTrackPageView from './useTrackPage';

export interface Filters {
	createdAt?: Date;
}

interface UserProductsPageProps {
	userId:number;
}

const UserProductsPage = ({ userId }: UserProductsPageProps) => {
    const [filters, setFilters] = useState<Filters>({
    	createAt: undefined,
    });

    const { data: user, isLoading: isLoadingUser } = useFetchUser(userId);
    
    const { data: products, isLoading: isLoadingProducts } = useFetchProduct(userId,filters);
    
    useTrackPageView({ pageId: 'user_products_page', data: { userId } }, [userId])
    
    const isLoading = isLoadingUser || isLoadingProducts;
    
    if (isLoading) {
    	return <div>Loading</div>
    }
    
    return (
    	<div className='tutorial'>
			<h1>hello {user!.name}! //'!의 의미는 널이 아님을 컴파일러에게 전달'
            <UserProductsFiltersButton onChange={setFilters}
           	{products && products.length ? (
            	<ProductsList products={produts} />
            ) : (
            	'No Products available.'
            )}
       </div>
   )
}

 

유튜버 분은 콜러와 콜리의 책임을 명확히하는 것을 통해 치명적인 버그를 피하기 위하는 방법들을 고민해야한다고 한다.

다른 말로 이야기하면 재사용성을 높이기위해, 유지보수를 좋게하기 위한 방법을 고민해야 한다.

 

많은 방법들 중 지극히 일부분이고, 지극히 초보적인 고민일 수도 있다.

 

개인적으로는 C언어와 CPP를 공부할때 책임소재 관련 문서들을 봤던 기억도 났고,

일하면서, 프로젝트를 하면서 했던 고민들이, 리팩토링을 통해 사라진 코드들이 생각나기도 했다.

 

좋은 개발자가 되기위해 내가 갖추고 싶은 것 중 하나가 좋은 결정을 하는 것이다.

많이 고민하고, 많이 쓰다보면 지금보다 더 좋은 결정을 할 수 있을 것이라고 믿는다. 

 

[출처]Refactoring a React component - Design Patterns: https://www.youtube.com/watch?v=PisA-OPisUY

+ Recent posts