오늘 배운 것

어제 프로덕션 서버를 배포하고, 이제 남은 작업은 배포된 프로덕션 서버를 사전에 구매한 도메인 주소와 연결시키면 된다. 그러기 위해서 Route53 서비스로 들어가 보았다. 

 

맨 밑에 있는, 구매한 도메인 이름 앞에 dev 서브도메인을 추가한 레코드는 현재 개발 서버와 잘 연결되어 있다. 사실 이 중에서 맨 밑에 CNAME 타입으로 연결된 레코드 빼고는 내가 연결한 게 아니고 어떻게 연결하는지도 잘 모르기 때문에... 일단 CNAME 타입의 레코드를 하나 더 생성해서 구매한 도메인과 연결해 보겠다. 

 

 

서브도메인 값을 아예 비워 두었더니 이런 오류가 났다. GPT에게 물어보니 CNAME 타입의 레코드는 서브도메인을 지정해야만 사용할 수 있으며, 루트 도메인(apex domain)에는 사용할 수 없다고 한다. 대신 ALIAS 타입의 레코드는 CNAME 타입과 유사하게 동작하지만 루트 도메인에서도 사용할 수 있다고 해서, 이 방법을 시도해 보았다. 

 

정확히는 레코드 타입에 'ALIAS'가 없어서, 'A' 타입을 클릭하고, '별칭 사용'을 활성화한 뒤, 다음과 같이 해당되는 리전, 해당되는 로드밸런서를 명시해 주었다. 

잘 된다!!!

잘 된다! 그런데 이 사이트는 HTTPS가 아니라 HTTP로 연결되는데, 이러면 보안 측면에서 불안전하다고 한다. 이 부분만 별도로 고칠 수 있다면 좋겠다. 

 

 

 

궁금한 점

1. Route53에 등록된 도메인을 어떻게 실행 중인 서버와 연결한다는 것일까? 생각해보면 도메인을 구매할 때 별도의 원격에서 잘 실행중인 서버를 구매하는 것은 아니고, 나중에 Route53 도메인에서 DNS 레코드의 값으로 지정할 수 있는 '값'을 받는다고 알고 있다. 내가 궁금한 것은 Route53에 도메인 레코드를 추가하거나 값을 바꿔주는 방식만으로 어떻게 구매한 도메인 주소를 입력하면 해당 로드밸런서의 DNS 주소로 연결이 되는 것인지가 궁금했다. 대략 찾아보니 여러 링크에 관련된 설명이 있는 것 같고 분명히 전공 지식에서도 배웠던 내용이라... 이 부분은 더 찾아보고 별도의 포스팅으로 올려야겠다. 

2. 왜 CNAME 타입의 레코드는 루트 도메인에는 사용할 수 없을까?

3. 찾아보니 ALIAS 타입 레코드는 CNAME 타입과 유사하게 동작하지만 루트 도메인에서도 사용할 수 있다고 한다. 이 둘의 차이는 또 무엇일까

4. 결과적으로 궁금한 점은 이 간단한 설정이 어떻게 도메인 검색을 여기까지 연결시켰느냐이다. 

5. HTTPS가 HTTP보다 보안에 좋다는 것은 알고 있는데 이걸 나 스스로 설명하지는 못하는 것 같다. 이것도 별도의 포스팅으로 만들어 보자. 

 

 오늘 배운 것

오늘 작업할 예정인 이슈들은 다음과 같다. 

1. 어제 작업하면서 알았던 인박스 뷰에서의 서브투두 날짜설정 이슈

2. prod와 dev로 개발환경 분리하기

3. prod 개발환경 실행시키기

 

1번은 팀원들이 모두 도착한 뒤에 논의가 가능하기 때문에 2번과 3번을 먼저 처리할 예정이다. 

 

기존의 onestep_be/settings.py의 파일의 내용을 전부 onestep_be/settings/base.py 파일을 만들어서 복사해준 뒤, 같은 디렉토리에 dev.py와 prod.py라는 파일을 만들었다. base.py 파일에는 dev와 prod 환경에서 공통적으로 사용하는 속성들을 모두 정의해 주고, 나머지 다른 설정을 dev랑 prod에만 해 볼 예정이다. 

 

또한 manage.py 및 asgi.py, wsgi.py에서 기본으로 장고의 세팅 파일을 어떤 파일로 참조할지 지정해주는 로직이 있다. 기본 값은 'onestep_be.settings' 였는데 이제는 파일의 경로가 변경되었으므로 'onestep_be.settings.dev' 경로로 바꿔주었더니 잘 동작하였다. 아래처럼 어떤 settings 파일을 사용할지를 이 코드에서 연결해주는 것 같아서 값을 바꿔주었다. 

 

이러면 python manage.py runserver 명령어를 입력했을 때는 DJANGO_SETTINGS_MODULE 변수에 기본값으로 할당된 dev 환경이 실행된다. 만약 runserver 명령어로 prod 환경을 실행시키고 싶다면, 명령어에 환경변수 값으로 prod 환경 파일의 경로를 넣어주면 된다. 

기본값 dev
prod 환경으로도 실행 가능

 

그리고 생각해보니 일반 개발환경에서는 python manage.py runserver가 실행되어야 하고, 나머지 경우에는 프로덕션 환경을 사용해야 한다. 그러므로 도커파일의 커맨드도 prod 환경으로 실행되는 커맨드로 바꿔주어야 하겠다. 그런데 또 생각해보니 개발서버는 dev 환경을 사용해야하고 프로덕션 서버는 prod 환경을 사용해야 한다. 그러면 도커파일의 커맨드도 각 브랜치별로 달라야 하는데... 이게 가능한가 싶기도 하다. 

 

그래서 또 생각해보니 굳이 dev 서버의 debug 모드를 true로 배포해야 할 필요가 있나 싶었다. 어차피 dev 서버를 호출하는 것도 프론트엔드의 앱일 텐데, 그러면 html로 디테일한 웹 메시지를 보여준다해도 어차피 api 통신 환경에서는 못 보기 때문이다. 혹시나 싶어 장고 공식문서의 settings DEBUG 속성을 찾아보았는데, DEBUG 속성을 true로 했을 때는 크게 두 가지 차이점이 있다고 한다: 

1. 에러가 발생했을 때 페이지로 오류의 구체적인 원인과 trace 등을 보여준다. 

2. 장고 서버에서 실행되는 모든 SQL 쿼리를 서버가 기억하고 있다. 이는 에러 트래킹을 하는 데는 유리하지만, 프로덕션 서버에서 사용할 경우 메모리를 많이 사용할 수 있다. 

 

아무튼 찾아보았는데 별 이상이 없어서, 도커파일에서 사용하는 커맨드를 기존의 python manage.py runserver에서 python manage.py runserver --settings=onestep_be.settings.prod로 바꿔 주었다. 


이제 프로덕션 환경에서 사용할 RDS를 생성해 주었다. 그 다음으로 AWS secrets manager에 방금 생성한 RDS의 퍼블릭 엔드포인트 주소와 사용자 이름 및 비밀번호를 넣어주어서 장고 서버에서 해당 값에 접근할 수 있도록 해 주면 된다. 그런데 이 작업을 하다보니 문제점이 생각났다. 처음엔 dev 서버와 prod 서버에서 모두 production 설정을 참조하면 되겠다고 막연히 생각했는데, 생각해보니 dev 서버에서는 개발 RDS DB를, prod 서버에서는 프로덕션 RDS DB를 참조해야 했다... 즉 이러나 저러나 dev 서버와 prod 서버는 다른 환경을 참조해야 하는 것이다. 

 

 

GPT에게 도움 요청을 해 보았다. GPT는 github action에서 사용하는 yml 파일에서 ECS 클러스터에서 사용할 태스크를 동적으로 정의하고, 그 과정에서 ECS 태스크의 환경변수로 DJANGO_SETTINGS_MODULE의 값으로 개발서버는 onestep_be.settings.dev를, 프로덕션 서버는 onestep_be.settings.prod를 값으로 주는 방법을 제시했다. 지금으로써는 이게 최선이지 않을까 싶어 이 방법을 적용해 보기로 했다. 

 

또한 yml 파일을 보니 프로덕션 RDS DB의 정보 말고도 추가로 필요한 정보가 있었다. 예를 들면 프로덕션 서버를 배포할 때 이미지를 올릴 ECR 레포지토리의 이름, ECS 클러스터의 이름 및 ECS 서비스의 이름이 필요해 보였다. 참고로 이 변수들은 AWS secrets manager 뿐만 아니라 github actions에도 secret으로 정의해야 yml 파일에서 인식할 수 있을 것 같았다. 이것들도 별도로 생성해준 뒤 yml 파일을 다시 작성해 보았다. 

 

그리고 생각해보니 개발환경과 프로덕션에서 사용하는 환경 변수가 다른데, 이걸 아예 별도의 보안 암호 이름을 두고 분리해야 하지 않을까 싶었다. 그래야 개발환경과 프로덕션 환경이 더 명확히 분리될 것 같았다. 그래서 기존의 개발환경에서 사용하던 onestep/dev는 그대로 두고, 프로덕션에서 사용하기 위해 onestep/prod 보안 암호를 생성해 주었다. 

 

그리고 production 세팅을 사용하도록 명령어에 값을 넣어준 뒤 runserver를 실행해 보았는데, 다음과 같은 에러가 났다. 아마 RDS DB에 접근하려는 database가 없기 때문인 것 같아서, RDS에 터미널을 통해 접속한 뒤 원하는 이름의 database를 생성해 주었다. 

 

다시 실행해 보니 잘 동작한다!

 

이제 다시 돌아가서 프로덕션 서버에서 사용할 ECS 레포지토리와 ECS 클러스터, ECS 태스크 정의를 생성해 주고, 로드밸런서를 생성해 주자. 그리고 ECS 서비스 설정을 마친 다음, 이 중에서 필요한 값들을 github action secret에 할당해 주자. 

 

main 브랜치(프로덕션 서버에 반영될 코드 내용)의 코드를 도커 이미지로 push할 ECR 레포지토리를 우선 만들어줬다.

 

그리고 해당 클러스터의 서비스에서 사용될 태스크 정의를 새로 만들어줬다. 

 

그리고 바로 ECS 서비스를 만드려고 했는데, 생각해보니 서비스를 만들려면 별도의 로드밸런서가 필요했고 로드밸런서를 생성하려면 별도의 대상 그룹이 필요했다. 그래서 대상 그룹부터 만들어주려고 했다. 

 

평화롭게 잘 만들어가던 중, VPC 서브넷의 IPv4 주소를 입력하라는 창이 떴다. 그래서 VPC 서비스로 가 보았다. 'VPC 서브넷의 IPv4 주소'가 무엇인지 모르겠어서 GPT에게 질문해보았다. 

 

GPT는 서브넷의 CIDR 블록에 있는 주소를 입력하라고 해서, 해당 주소를 입력해 주었다. 그렇게 대상 그룹을 생성하고, 로드밸런서를 생성하고, 서비스를 순차적으로 생성했다. 지금은 ECR 레포지토리에 올라온 도커 이미지가 아예 없어서 아마 ECS 서비스가 태스크를 실행할 수 없을 것이다. 

 

그리고 github action secrets에 관련 파일도 올려두었다. 기존에 개발환경에서 사용하는 관련 변수들 뒤에 _PROD를 붙여서 해당 변수들은 프로덕션 환경에서 사용하게끔 yaml 파일도 작성했다. 

이런 값들이 들어있다

 

처음엔 잘 돌아가나 싶더니 뭔가 이상했다. 10분이 지나고 ECS의 서비스를 확인해 봤는데 태스크가 실행되고 있지 않았다. 

 

혹시나 싶어 ECR에 도커 이미지가 올라왔나 확인해봤는데 올라오지 않았다. 아무래도 yaml 파일에서 ECR 레포지토리에 이미지를 업로드하는 과정에서 뭔가가 오류가 있는 듯 하다. 이 오류는 집에 가서 정신이 멀쩡해지면 다시 해결해 보겠다. 


오류가 아니었다! 집에 와서 확인해보니, 21분 만에(...) develop 브랜치에 있는 내용이 ECR을 거쳐 ECS를 통해 배포되었다... 근데 이게 과연 성공이라고 말할 수 있을지 모르겠다. 지금은 무중단 배포도 아닌 걸로 알고 있는데 2초도 아니고 20분이 붕 뜬다..? 지금 배포되는 방식이 중단 배포인지 무중단 배포인지 알아보고, 중단 배포라면 무중단 배포 방법을 알아보아야겠다. 

21분 실화냐

암튼 이 과정이 완료되고 나서, 기대에 부푼 마음으로 main 브랜치의 workflow를 실행시켜 보았다. 여기서는 에러가 났다. 로그를 보니 ECS 서비스 안의 컨테이너 이름이 yaml 파일과 일치하지 않은 것 같았다. 

 

알고보니 yaml 파일에 오류가 난 task 부분의 container name이 개발 서버에서 사용하는 컨테이너 이름으로 되어 있어서 바꿔줬다. 

 

다시 띄워보니 성공했다!

 

ECS 클러스터>서비스>태스크>네트워크의 ENI 정보>퍼블릭 DNS로 접속하니, 접속이 잘 되었다.

 

그런데 로드밸런서에 설정해 놓은 health check url로 들어가니 swagger가 나오지 않고 다음과 같은 django login 창이 떴다. 

 

현재 개발서버의 똑같은 URL로 들어가면 swagger 화면이 잘 나오기 때문에, 이는 prod와 dev의 환경 중 일부 값이 다른 것이 원인인 것 같다. 어쩌면 DEBUG 옵션일 수도 있겠다. 어쨌건 서버는 잘 뜨니 이 문제도 추후 확인해 보면 좋겠다. 

 

 궁금한 점

1. 이렇게 환경변수를 명령어로 할당하는 것 말고 다른 방식으로도 필요할 때만 prod 환경 설정을 사용하는 방법이 있을지 궁금하다. 

2. 로드 밸런서를 생성할 때 VPC와 서브넷과 로드밸런서가 구체적으로 어떤 관계가 있는 것인지 잘 모르겠다. 

3. DEBUG=False 옵션과 /swagger UI에서 django login 화면이 뜨는 것이 관련이 있는지 궁금하다. 없다면 어떤 원인일지도 궁금하다. 

 오늘 배운 것

오늘은 어제 작업하다가 미처 처리하지 못한 인박스 뷰의 남은 이슈와, 프론트에서 다른 팀원들과 협업이나 논의가 필요한 일부 버그 빼고 나머지 기능들이 잘 동작하는지를 확인하는 것이 목표였다. 그리고 백엔드 이슈의 경우는 이제는 ECS에서 fargate로 잘 돌아가고 있는 서버와 사전에 구매해둔 도메인을 AWS의 Route53을 이용해서 잘 연결해 두었었다. 문제는 지금까지 연결한 도메인은 앞에 'dev.'가 붙은 개발 환경 도메인이었고, 실제 개발 환경과 프로덕션 환경 최소 2개는 있어야 된다고 멘토님이 사전에 말씀하셨었다. 그래서 dev.를 뺀 도메인을 뉴 서버랑 연결해서 해당 이슈만 처리하면 급한 불은 끄게 되겠다. 

 

그러니까 크게 작업해야 할 부분은 세 가지겠다. 

1. 인박스 뷰 남은 이슈 처리

2. 프론트 최종 점검

3. 백엔드 프로덕션 환경 연결하기

 

우선은 1번 이슈인 인박스 뷰를 작업해 보려고 했다. 어제 못 처리하고 남았던 이슈는 두 가지였다. 

1-1. 인박스 뷰에서 카테고리가 달라짐에 따라 다른 투두들이 나타나도록 구현하기

1-2. 인박스에 있던 투두에서 날짜를 설정해 주면 인박스 뷰에서 사라지고 해당 날짜에 뜨도록 하기

 

1-1번 이슈의 경우, 인박스 리스트 뷰(InboxTodos)의 useEffect를 사용하는 부분에서 dependency array에 selectedCategory 변수를 추가해주니 해결되었다. 즉 selectedCategory(선택된 카테고리)의 값이 바뀔 때마다 보여줄 투두 리스트를 다시 필터링하도록 변경해 준 것이다. 

 

1-2번 이슈의 경우, 인박스에 있던 투두에 날짜를 설정하고 해당 날짜로 가 보니 투두가 잘 나왔다. 그런데 다시 인박스로 가 보니 투두가 안 사라지고 그대로 있었다. 인박스에 있는 투두를 보여주는 API를 호출해 보니 투두에 startDate와 endDate 값은 null이 아닌 설정한 날짜로 잘 할당되어 있었는데 인박스 API에 투두가 나타나는 오류가 있었다. 내일 다른 팀원에게 이슈를 공유하고 이 오류를 해결한 다음, 다시 잘 작동하는지 확인해 보면 되겠다. 


그래서 1번은 일단 이렇게 마무리하고, 2번 이슈를 작업하면서 1번에서 해결하지 못한 이슈들을 제외하고 또 오류가 나는 기능들이 있는지 살펴보았다. 그랬더니 완료/미완료 상황이 다음과 같았다. 

 

특히나 문제인 부분은 하위투두의 날짜를 변경하는 부분이었다. 현재 프론트 레포에서는 DailyTodos라는 리스트 컴포넌트 안에 DailyTodo라는 개별 컴포넌트가 있고, 그 안에 DailySubTodo라는 하위 투두 컴포넌트가 있다. 그러니까 어떤 날짜에 대한 투두를 불러올 때 그 날짜 안에 해당되는 투두가 없다면 당연히 서브투두는 불러와지지 않는 거였다. 이 로직은 당연히 필요하고 또 맞다고 생각한다. 

 

그러나 문제는 현재 캘린더 기준으로 날짜 필터링이 프론트에 구현되어 있지 않다는 거였다. 캘린더 모달창은 나타나는데, 예를 들어서 하위 투두의 날짜변경을 할 때, 상위투두의 startDate랑 endDate 사이의 날짜만 선택되도록 강제하는 기능을 구현하지는 않았었다. 그래서 그 외의 날짜를 사용자가 선택할 경우, 투두가 제 날짜에 나타나지 않는 거였다. 또한 그렇게 해서 날짜이동을 한 하위투두가 이전 날짜에서도 그대로 보였다. 이것은 프론트에서 서브투두를 맞게 필터링해야 하는 것으로 보인다...

 

그런데 이렇게 문제가 복잡하다 보니 작은 부분의 기능이나 UI를 위해서 너무 많은 것들(DB 설계, API, 프론트 필터링 로직 및 상태관리 등)이 필요하다는 생각이 들었다. 우리의 MVP에서 사실 상위 투두가 꼭 여러 날짜에 걸쳐서 진행되는 기능은 핵심 기능은 아니라는 생각이 들었다. 하지만 이것은 내 개인적인 생각일 뿐이라서, 내일 팀원들과 만나서 이 개인 이슈를 공유해 봐야겠다는 생각이 들었다. 

 

그래서 2번 이슈는 일단은 이렇게 일단락이 되었다. 논의가 길어지면 기획과도 연결될 수 있고 무엇보다 당장 확정된 합의가 없는데 이걸 섣불리 적용했다가 다시 엎게 되는 상황만은 피하고 싶어서... 일단은 안 손대고 공유만 하는 것이 맞겠다는 생각이 들었다. 이럴 때 어떻게 대응해야 할지도 멘토님께 다음에 여쭤봐야겠다. 일단 PR까지는 올려두었다. 


이제 남은 것은 3번 이슈이다. 작게는 이런 하위 이슈로 나눌 수 있을 것 같다. 

 

3-1. 프로덕션 환경용 RDS 인스턴스 시작 및 연결

3-2. 깃허브 액션의 기존 yaml 파일에서 타깃 브랜치를 develop으로 변경

3-3. 프로덕션 서버에서 사용할 로드밸런서 생성

3-4. main 브랜치에서 새로 ECR->ECS에 fargate로 서버 띄우는 yaml 파일 작성

3-5. 엔드포인트로 잘 접속되는지 확인

3-6. 도메인 연결하기(앞에 dev 뺀 도메인으로 연결하기)

 

이 이슈는 저녁을 먹고 나서, 다시 멀쩡해진 정신으로 처리해 봐야겠다. 

 오늘 배운 것

어제 포스팅에서 'Moment'와 'Date' 클래스가 서로 달라서 한 클래스에서 사용 가능한 메소드를 다른 클래스에서 사용하지 못하는 것은 맞지만, 구체적으로 어떻게 다른지는 알아보지 못한 것 같았다. 

 

moment의 경우 javascript 환경에서 사용할 수 있는 라이브러리에서 제공하는 클래스이며, date는 javascript에서 기본으로 제공하는 클래스이다. date와 moment의 가장 큰 차이 중 하나는 moment는 여러 timezone에서 사용 가능한 반면 date는 기본 UTC 시간대에서만 동작한다는 것이다.

 

그리고 moment의 공식문서를 읽다가 발견한 내용인데... 여러 timezone을 지원해야 할 경우 moment를 사용할 때 size(정확히 어떤 크기를 말하는것인지는 잘 모르겠다..!)가 커질 수 있다는 문제가 있다고 한다. 그래서 moment를 개발한 사람들이 더 이상 moment의 기능을 고도화하지 않을 예정이며, 오히려 Luxon같은 타 라이브러리로 쉽게 옮겨갈 수 있도록 지원한다는 내용도 있었다. 

공식문서에서 가져온 내용

 

어제 기존 코드와 바꾸려고 한 코드의 클래스 타입이 다르다는 것을 안 이유도, convertGmtToKst라는 함수를 사용하면 Date 객체를 리턴하는데, 이 객체는 moment에서는 제공하는 isSame()이라는 함수를 사용할 수 없기 때문이었다. 소스코드를 보니 moment에서 제공하는 기본 내장 함수들이 더 많긴 하더라. 캡처한 것보다 훨씬 많고 몇십 개 쯤 되었다. 

moment에는 isSame()이라는 시간대 비교 메소드가 있다
Date에는 없다(이 세개가 전부)

암튼 그러하고, moment의 공식 문서에서도 moment보다는 date-fns나 Luxon의 사용을 권장하고 있는 점이 신기했다. 하긴 모든 라이브러리들은 사용자의 편의성을 위해 만들어졌지... 더 편리한 다른 라이브러리가 있다면 사용자에게 그것을 권장하는 것도 맞겠다는 생각이 들었다. 그래서 현재는 일단 도입한 moment를 잘 사용하되, 공식문서에서 언급했던 그런 문제점들을 실감하게 된다면 위에서 언급한 다른 라이브러리를 고려해봐도 좋을 것 같다. 


오늘 처리하기 위한 프로젝트 이슈는 '인박스 뷰 만들기'이다. 하나의 뷰를 만드는 것이지만 기존에 만들어진 투데이 뷰의 UI나 로직을 대부분 그대로 가져와서 쓰기 때문에 실제로는 하나의 뷰를 바닥부터 만드는 것보다는 훨씬 적은 시간이 들 것 같다. 

 

다만 상태관리나 세부 컴포넌트 부분에서는 변경해 줄 부분들이 꽤 보였다. 첫째로, 인박스 뷰에서는 현재 선택된 날짜를 관리하는 상태 변수인 selectedDate 변수가 필요 없었다. 왜냐하면 인박스 뷰는 날짜들이 지정되지 않은 모든 투두들을 모아 보여주는 뷰이기 때문이다. 둘째로, 기존의 투두는 왼쪽 체크 버튼을 누르면 완료 처리가 가능했는데, 인박스에서는 투두를 완료 처리할 수 없도록 해야 했다. 그러려면 기존에 사용하던 DailyTodo라는 컴포넌트 대신에 다른 컴포넌트를 별도로 정의해서 사용해주어야 했다. 마지막으로, 인박스에서도 투두 컴포넌트를 클릭하면 모달창이 떠서 해당 투두를 수정하거나, 삭제하거나, 오늘 하거나, 다른 날 할 수 있도록 지정하는 기능이 필요했다. 

 

위 기능들을 구현하기 위해서 InboxTodos 컴포넌트를 만들었다. InboxTodos(인박스 투두 리스트를 보여주는 컴포넌트)와 DailyTodos(데일리 투두 리스트를 보여주는 컴포넌트)의 가장 큰 차이점은 현재 어떤 날짜가 선택되었는지를 나타나는 selectedDate라는 상태 변수가 InboxTodos에는 없다는 거였다. 사실 최대한 DailyTodos 컴포넌트를 재사용하고 싶었는데, 괜히 inbox={true} 같은 속성을 넣는 것보다는 별도의 컴포넌트로 정의하는 것이 더 상태관리가 복잡해지지 않는 방향이 아닐까 싶어서 이 방식을 선택했다. 하지만 이 역시 좋은 방향인지는 확실하지 않으니 다음 멘토링 때 멘토님께 조언을 구해보는 것이 좋겠다.

 

그리고 인박스 뷰에서 투두 컴포넌트를 사용하기 위해서는 InboxTodo 컴포넌트를 별도로 만들기로 했다. 사실 DailyTodo 컴포넌트에 inbox 속성을 추가해서 inbox이면 기본값인 체크 아이콘을 다른 아이콘으로 변경하는 방법도 있는데, 그러면 inbox 속성값이 true일 때는 DailyTodo 컴포넌트에 정의해 둔 제법 많은 상태관리 로직들이 아예 쓰일 일이 없었다. 그보다는 InboxTodo라는, 보이는 것은 거의 비슷하지만 상태관리 로직은 거의 없는 컴포넌트를 하나 만드는 것이 더 좋아 보였다. 

 

또한 모달창의 경우는 원래 있던 TodoModal 컴포넌트에 inbox 속성을 추가해보기로 했다. 굳이 모달창만 속성을 추가해서 재사용하고 나머지 InboxTodos나 InboxTodo 컴포넌트는 따로 구현하는 이유는, 내가 생각했을 때 상태관리 로직의 차이가 크기 때문이다. 즉 모달창은 인박스 뷰에서 쓰나 투데이 뷰에서 쓰나 상태관리 로직이 사용되는 정도는 비슷했다. 그러나 나머지 경우, 만약 컴포넌트를 재사용하고 인박스 뷰에서 쓸 경우, 투데이 뷰에서는 쓰이는 여러 상태관리 로직들이 inbox 속성값이 true가 되면 쓰이지 않게 되어버리는데, 이게 맞는 방향인가? 라는 의문이 들었다. 물론 이 논리에는 구체적인 이유나 근거는 없어서... 다음 멘토링 때 프론트 멘토님께 이런 판단이 괜찮을지 조언을 구해봐야겠다. 


백엔드 서버를 로컬에 띄워두고, 프론트 서버에서는 localhost로 요청을 보내면서 작업하고 있었다. 아직 개발 단계라 가끔씩 API와의 통신 오류 등이 날 때 로그를 확인하려면 이 방법이 더 편했기 때문이었다. 그런데 잠시 쉬고 저녁을 먹었다가 다시 작업하는데 이러한 오류가 났다. 이전에도 종종 있었던 오류로, Jwt에 있는 토큰 발행 시각(iAt, issuedAt)보다 컴퓨터의 현재 시각이 이르기 때문에 발생하는 오류였다. 

 

그래서 현재 컴퓨터에서 시간을 올바르기 가리키고 있는지도 잘 확인해 보았다. 이전에 멘토님께 한번 이 문제를 말씀드렸더니, 어쩌면 컴퓨터가 아니라 사용하는 에뮬레이터의 시간 설정에서 문제가 있을 수도 있다고 말씀해주셨었다. 그도 그럴 것이 어떨 때는 잘 되고, 또 다시 에뮬레이터를 재시작하면 안 되고... 그런 문제였기 때문이다. 만약 맥북의 시간 설정에 문제가 있었다면 오히려 항상 이런 오류가 떴어야 하겠다. 

 

GPT에게 물어보니 장고 서버와 맥북의 timezone을 제대로 설정해 보라는 식으로 답변해줬다. 맥북은 이미 timezone이 올바르게 설정되었는지 확인했으니, 장고 서버의 settings.py의 TIME_ZONE 변수의 값을 UTC에서 Asia/Seoul으로 바꿔 주었다. 오류가 어쩌다가 일어날 때도 있고 아닐 때도 있어서(...) 이 방법으로 완전히 해결된 것인지는 잘 모르겠지만, 일단 설정을 고쳐주니 잘 동작하였다. 

TIME_ZONE = 'Asia/Seoul'

 

그리고 공식문서를 찾아보니 장고에서 timezone을 다룰 때, DB에는 UTC 시간으로 저장해 놓은 다음 장고 서버 내부에서 객체에 timezone을 적용해서 end user에게 필요할 경우 그 값을 리턴해 준다고 나와있었다. 그리고 만약 서버에서 timezone을 이렇게 내부적으로 처리하지 않고, 그냥 timezone을 고려하지 않고 단일 UTC를 기준으로 사용하고 싶다면 settings.py에서 USE_TZ 변수값을 False로 바꿔주면 된다고 한다. 

 

그리고 설정을 잘 찾아보면 시간값을 DB에도 UTC가 아닌 다른 timezone으로 저장할 수 있는 것 같았지만 이를 권장하지는 않는다고 했다. 이유가 여러 나라에서 DST(daylight saving time)를 사용하기 때문이라는데, 설명을 읽어보니 DST는 하절기에는 시간이 더 앞으로 이동하고, 동절기에는 더 뒤로 이동하도록 조정한다는 것 같았다. 그래서 시간값을 UTC가 아닌 다른 timezone으로 지정하면 이렇게 1년에 두 번, 시간을 앞으로 또는 뒤로 더 이동시키는 시점에 timezone 관련 오류가 날 수 있다고 나와있어서 매우 신기했다. 

 

그리고 항상 잘 사용하던 파이썬의 datetime.datetime 객체에는 tzinfo라는 timezone 정보를 저장하는 속성이 있다고 한다. 만약 settings.py에 USE_TZ 설정이 True로 되어있다면, 이 속성을 통해 앞서 언급한 것처럼 datetime 등의 object에서 timezone 정보를 처리하는 것으로 이해했다. 즉 USE_TZ 설정이 True라면 생성되는 datetime 객체들은 전부 timezone-aware(timezone에 대한 정보를 갖고 있음)인 것이고, 반대의 경우 timezone-naive(timezone에 대한 정보를 갖고 있지 않음. 기본값은 UTC)인 것이다. 

 

즉 정리해보면 만약 엔드 유저에게 timezone 관련 정보를 리턴해야 할 경우, 그리고 USE_TZ 값이 true로 되어있는 경우, 처음에 DB에서는 UTC로 저장된 값을 꺼내온다. 그러나 datetime 타입 등의 객체를 사용하면서 장고 서버에서 자체적으로 이 객체 안에 tzinfo라는 속성을 넣어줘서 timezone을 인식할 수 있게 한다. 객체가 인식하는 timezone은 아마도 위에서 settings.py에 설정한 TIME_ZONE 변수의 값일 것이다. 그러면 해당 객체를 serializer 등을 통해서 응답으로 리턴할 때 엔드 유저에게는 timezone이 적용된 시간값이 리턴된다고 볼 수 있다. 

 

 궁금한 점

1. timezone에 관련해서 내가 이해한 것이 정확할까 궁금하다. 

'개발 일기장 > SWM Onestep' 카테고리의 다른 글

20240805 TIL: dev와 prod 환경 분리하기  (0) 2024.08.05
20240804 TIL  (0) 2024.08.04
20240802 TIL: 코드 참고하면서 react query로 api 호출하기  (2) 2024.08.02
20240801 TIL  (0) 2024.08.01
20240731 TIL  (0) 2024.07.31

 오늘 배운 것

react query의 동작 방식에 대해서는 여전히 잘 모르지만, 일단은 코드를 작성해야 하는 상황이다. 다행히 다른 팀원이 작성해 둔 코드가 있어서 그걸 참고해 보려고 한다. 오늘 개발해야 할 부분은 투두나 서브투두를 클릭할 때 나오는 모달에서 투두나 서브투두를 삭제하는 로직을 react query로 작성하는 것이다. 기존 코드는 fetch()를 사용해서 API를 호출하고 있었는데, 이는 똑같이 동작하긴 하지만 중복된 코드가 너무 많아서 코드가 복잡해진다는 단점이 있었다. 그래서 똑같이 동작하는 코드를 다르게 짜면 되겠다. 

 

우선 투두 모달(TodoModal.jsx) 컴포넌트에서 사전에 작성해 둔, 필요한 리액트 커스텀 훅(useTodoDeleteMutation)을 불러온다. 이때 useTodoDeleteMutation이라는 커스텀 훅은 tanstack query의 useMutation()을 사용해서 만든 커스텀 훅으로, mutate라는 함수를 기본적으로 리턴한다. 이 {mutate: deleteTodo} 부분은 해당 함수를 mutate라는 이름으로 사용하면 여러 커스텀 훅을 사용할 때 헷갈릴 수 있으니, 함수에 deleteTodo라는 alias(다른 이름)를 준다는 의미이다. 

 

그리고 deleteTodoIsSuccess는 마찬가지로 해당 커스텀 훅에서는 훅이 성공적으로 실행되었는지의 여부를 isSuccess라는 변수에 담아 리턴하는데, 한 컴포넌트 내에서 여러 훅을 사용하는 경우 이름이 중복될 수 있으니 해당 변수에 deleteTodoIsSuccess 라는 alias를 준다는 의미이다. 

const { mutate: deleteTodo, isSuccess: deleteTodoIsSuccess } = useTodoDeleteMutation();

 

그 다음에 해당 컴포넌트(TodoModal) 내에서 useEffect 기본 훅을 사용해서, 훅이 성공적으로 실행되면 실행되게 하고 싶은 로직을 안에 정의해 준다. 

useEffect(() => {
    if (deleteTodoIsSuccess) {
      queryClient.invalidateQueries(TODO_QUERY_KEY);
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [deleteTodoIsSuccess]);

 

그러면 이제 컴포넌트 내에서 해당 커스텀 훅을 사용할 수 있고, 훅이 성공적으로 실행되었을 경우 추가로 다른 로직이 실행되도록 할 수도 있다. 이제는 아까 deleteTodo라는 alias로 받아온 함수를 직접 사용해주면 된다. 

const handleDelete = async item_id => {
    if (isTodo) {
      deleteTodo({ accessToken: accessToken, todoId: item_id });
    } else {
      deleteSubTodo({ accessToken: accessToken, subTodoId: item_id });
    }
    setVisible(false);
  };

 

해당 함수에 파라미터를 넘겨줄 때는, 함수의 커스텀 훅(useTodoDeleteMutation)의 mutationFn이라는 속성의 값으로 정의된 함수(아래에 나올 deleteTodoFetcher)에서 명시되어 있는 파라미터를 그대로 넘겨줘야 한다. 

 

이제 커스텀 훅인 useTodoDeleteMutation의 코드를 보자. 

const deleteTodoFetcher = async ({ accessToken, todoId }) => {
  const data = await Api.deleteTodo({ accessToken, todoId });
  return data;
};

export const useTodoDeleteMutation = () => {
  return useMutation({
    mutationFn: deleteTodoFetcher,
    onError: error => {
      console.error('Error deleting todo:', error);
    },
  });
};

 

이 커스텀 훅에서는 useMutation()이라는 tanstack query의 내장 함수를 이용해서 위의 컴포넌트에 mutate 함수와 기타 props(isSuccess 등)를 리턴해 준다. 이때 useMutation()을 사용하면서 기본적으로 mutationFn이라는 속성을 정의해 주어야 하는데, 이는 비동기 태스크를 실행하고 Promise를 리턴할 함수, 즉 useMutation으로 실행하고 싶은 비동기 함수의 이름이다. 

공식문서에서 나온 useMutation에서의 mutationFn의 소개

 

여기서는 deleteTodoFetcher이라는 비동기 함수를 선언하였고, 안에는 Api라는 레포에서 다른 팀원이 따로 만들어 둔 클래스를 통해 API 서버의 deleteTodo API를 호출하는 로직이 담겨져 있다. 여기에서 파라미터로 accessToken과 todoId를 받기 때문에, 앞서 컴포넌트에서 accessToken과 todoId를 파라미터로 넣어 준 것이다. 

 

마지막으로 Api의 deleteTodo 로직을 보면 다음과 같다. 

deleteTodo: ({ accessToken, todoId }) => {
    return handleRequest(() =>
      axios.request({
        url: API_PATH.todos,
        method: 'DELETE',
        headers: metadata(accessToken),
        data: { todo_id: todoId },
      }),
    );
  },

 

handleRequest 역시 따로 개발하면서 정의해 둔 함수로, 함수를 호출할 때 에러 처리를 하도록 try/catch로 한번 감싸 준 함수라고 생각하면 된다. 

 

즉 앞으로 API 서버에서 새로운 API를 호출하고 싶다면, Api 클래스에서 새 함수를 정의하고, 이를 커스텀 훅으로 만든 다음, 이걸 사용하려는 컴포넌트에서 훅을 호출하는 로직을 작성해 주면 되겠다. 여러 파일을 거쳐서 처음에 로직을 파악하는 것은 조금 복잡할 수 있지만, 이전의 한눈에 보기 불편하고 중복이 많은 스파게티 코드보다는 훨씬 나은 방향인 것 같다. 

 

그리고 PR을 올린 다음, 휴가를 즐기고 몇 시간 뒤 댓글로 달린 P1, P2, P3 리뷰들 중 내가 생각하기에 이번 PR에서 꼭 개선되어야 하는 부분들을 추가로 반영했다. 대표적인 것이 selectedDate라는 상태 변수의 timezone 문제였다.

 

이전에 비슷한 문제가 있어서 GPT에게 해결 방법을 물어봤었는데, GPT는 복잡한 설정을 할 바에는 직접 timezone을 바꾸는 함수를 utils에 만들어서 사용할 것을 권해줬다. 그도 그럴 것이 UTC 표준 시간에서 9시간만 더하면 딱 한국 시간대이기 때문이다. 그래서 이런 함수를 utils에 만들어 두고, 그때 당시 담당했었던 캘린더로 투두 날짜를 변경해줄 때 잘 사용했었다. 

 

기존에 WeeklyCalendar에서 주간 캘린더에 사용될 날짜를 나타내는 코드는 다음과 같았다. 

const getWeekDates = date => {
    const start = date.clone().startOf('ISOWeek');
    const r = Array.from({ length: 7 }, (_, i) =>
      start.clone().add(i, 'days'),
    );
    return r;
  };

 

그런데 생각해보니 WeeklyCalendar이라는 날짜 컴포넌트에서도 selectedDate 변수를 사용하고 있었는데, 이 컴포넌트에서는 해당 함수를 사용하고 있지 않았다. 로그를 찍어보니 시간이 자정 시간보다 9시간 느린 오후 15시를 가리키고 있었다. 그래서 냅다 utils에 정의한 'convertGmtToKst' 라는 함수를 적용해서 바꿔주려고 했다. 그런데 오류가 났다. 

const getWeekDates = date => {
    const start = date.clone().startOf('ISOWeek');
    const r = Array.from({ length: 7 }, (_, i) =>
      convertGmtToKst(start.clone().add(i, 'days')),
    );
    return r;
  };

 

 

배열 r의 원소 타입과 기존에 사용되던 코드의 원소 타입이 같지 않아서 생기는 문제인 것 같았다. typeof 연산자로는 해당 변수가 object라는 정보밖에는 알 수가 없어서, 어떻게 다른지 알기 위해서는 구체적인 클래스 이름을 알아야 했다. 

console.log(r[0].constructor.name);

 

그랬더니 이전 코드에서는 'Moment'가, 이후로 바뀐 코드에서는 'Date'가 나왔다. utils에 정의한 convertGmtToKst 함수는 Date 객체를 리턴하고 있었는데, 기존의 WeeklyCalendar에서는 moment()를 사용하고 있기 때문에 moment에서 사용 가능한 메소드를 Date 객체는 사용할 수 없었던 문제였다. 

 

최종적으로 코드를 이렇게 바꿔서 Date 객체를 다시 moment로 바꿔주니 잘 동작하였다!

const getWeekDates = date => {
    const start = date.clone().startOf('ISOWeek');
    const r = Array.from({ length: 7 }, (_, i) =>
      moment(convertGmtToKst(new Date(start.clone().add(i, 'days')))),
    );
    return r;
  };

 

그리고 위와 비슷한 방법들로 이번에는 투두 모달창 안에서 동작하는 기능이 아닌, 모달창 밖에 있는 투데이 뷰에서 동작하는 기능들에 대해서 유사한 작업(버그 수정, 불필요한 코드 dependency 제거, 기존 로직 react query 로직으로 변경 등)을 해서 마저 다른 이슈를 완료하였다. 이제 내일은 이 유사한 로직을 기반으로 인박스 뷰를 작업할 예정이다. 

 

'개발 일기장 > SWM Onestep' 카테고리의 다른 글

20240804 TIL  (0) 2024.08.04
20240803 TIL: moment, date, timezone  (0) 2024.08.03
20240801 TIL  (0) 2024.08.01
20240731 TIL  (0) 2024.07.31
20240730 TIL: RN에서 UI Kitten으로 모달에서 캘린더 띄우기  (0) 2024.07.30

 오늘 배운 것

오늘은 원래는 데모데이로 예정된 날이었으나, 예상보다 작업을 하면서 걸리는 시간이나 추가로 해야 할 하위 이슈들이 많아졌다. 그래서 일단은 회의실을 빌려서 팀원들과 현재 작업이 어디까지 진행되었고, 어느 부분에서 막히며 어떻게 얼마 안에 처리할 수 있을지를 논의해보는 중간점검의 장으로 활용하려고 한다. 

 

우선 그저께부터 길어지던 프론트 리팩토링 이슈에 대해서 논의해보았다. 이 이슈에 은근 시간을 많이 쏟게 되고 있고, 근데 이게 또 파라미터가 제대로 전달이 안 되거나 전달 형식이 잘못된 문제라서 우리가 지금 시도해 볼 수 있는 게 괄호를 빼 보고, 콘솔 로그를 찍어 보고... 요런 것 뿐이라서 좀 원시적인 방법으로 접근하고 있는 것은 아닌가 싶었다. 하지만 일단은 해 볼 수 있는 데까지는 해 보는 게 낫겠다. 그리고 어차피 이 프론트 코드 리팩토링 이슈는 지금 흐린 눈을 하고 덮고 가더라도 분명 9월에도 더 큰 스노우볼이 되어 발목을 잡을 것이기에, 지금 해결하는 것이 장기적으로도 좋겠다. 

 

암튼 그래서 프론트에서 서버로 유저의 정보를 리턴해주는 API를 호출하는 부분에서 401 에러가 뜨는 부분을 팀원과 같이 다뤄보았다. 처음에는 프론트에서 헤더의 액세스토큰 파라미터를 잘못 보내는 것일 수도 있을 것이라 생각하고 작업하였으나, 모든 부분에 로그를 찍어 보았을 때 헤더가 멀쩡하게 전달되고 있었다. 그래서 이 가능성보다는 그렇다면 백엔드에서 해당 엑세스토큰이 담긴 헤더를 전달할 때 401 에러를 내고 있는 것이 아닌가? 라는 추측을 하게 되었다. 

 

에러의 원인을 찾았는데, 바로 코드에서 어떤 API를 호출할 때는 axios를 사용하고, 다른 API를 호출할 때는 fetch를 사용하도록 되어 있었다. 그래서 axios에서는 {}로 바인딩 된 헤더가 필요했던 것이고, fetch에서는 헤더가 바인딩되면 이를 인식하지 못했던 것이었다. 그래서 이 문제는 무탈하게 넘겼다. 

 

문제는 그 다음이었다! 이제 로그인까지는 잘 되고, TodayView에서 카테고리를 생성해야 한다. 그런데 Category를 생성하는 API에서, post API로 200 코드는 잘 받아오는데 문제는 그리고 나서 다시 메인 화면으로 돌아가지 않는다는 거였다. 

 

코드는 이런 식이다. useCategoryAddMutation 이라는 커스텀 리액트 훅 함수가 실행되고 나서, 이 함수가 성공적으로 실행되면 isSuccess라는 prop의 값이 false에서 true로 변한다. 그리고 초기의 의도는 그러고 나서 onSuccess prop 안으로 넘긴 로직이 실행되는 것이었다(category 이름을 공백 문자열로 바꾸고, 다시 router로 뒤로가기).

const {
    mutate: addCategory,
    isLoading,
    isError,
    error,
    isSuccess,
  } = useCategoryAddMutation({
    onSuccess: () => {
      setCategoryName('');
      router.back();
    },
  });

 

그런데 이렇게 하면 isSuccess의 값이 제대로 반영되지 못하는 것 같아서, 로직을 바꿔보았다. 역시나 되지 않는다. 하지만 GPT는 잘 했다고 해서, 공식문서를 보아야 할 것 같다. 

useEffect(() => {
    if (isSuccess) {
      setCategoryName('');
      router.back();
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [isSuccess]);

 

일단은 중간까지 작업한 부분을 다른 팀원의 코드에도 반영하기 위해 PR을 날렸다. 

사연 있는 PR 제목


문제를 해결하였다!

팀원들과 열심히 삽질하면서 알아낸 원인은 tanstack query v5에서 더 이상 useMutation() 함수를 사용할 때 더 이상 onSuccess라는 prop을 인자로 받지 않았기 때문이다. 그래서 기존의 useCategoryAddMutation 커스텀 훅 함수에서 onSuccess prop을 넘겨주는 코드를 제거하고, 대신 isSuccess 라는 훅이 성공했는지를 알려주는 변수를 받아서 useEffect()에서 isSuccess 값이 바뀔 때 원하는 로직을 실행하도록 바꿨다. 

 

그런데 사실 이는 tanstack query v5 공식문서와는 반대되는 결과였다. 공식문서에서는 onSuccess라는 prop을 멀쩡히 useMutation() 함수가 잘 받고 있는데, 실제로는 그렇지 않았던 것이다. 알고보니 공식문서의 다른 부분에 우리가 사용하려는 부분이 명시되어 있었다. 공식문서를 잘 봐야 함을 느낀 이슈였다. 

 

이제는 API 서버에서 받아온 todos의 데이터를 프론트 화면에서 보여주는 작업을 하면 된다. 그런데 투두를 만드려면 파라미터로 order의 값을 보내줘야 했다. 우리 서비스에서는 드래그 앤 드롭 순서정렬을 하려는 용도로 LexoRank라는 알고리즘을 사용한다. 이를 사용해서 이 order를 어떻게 정하면 좋을지 같이 논의해보았다. 일단은 임시 값으로, 현재 등록된 투두가 아무 것도 없다면 lexorank 알고리즘을 사용해서 middle, 즉 중간값을 내려준다. 만약 등록된 투두가 하나라도 있다면 그 투두의 다음 order 값을 genNext()를 통해 내려주도록 했다. 

 

아, 그리고 저녁을 먹고 와서 보니 API 서버에서 받아온 todos의 데이터가 나타나지 않는 이유는, render 함수에서 값을 리턴하고 있지 않기 때문이었다. 즉 코드를 이렇게 바꿔 주니 잘 동작하였다. 

 

(before)

const renderTodo = ({ item, drag, isActive }) => {
    <View>
      <DailyTodo item={item} drag={drag} isActive={isActive} />
    </View>
};

(after)

const renderTodo = ({ item, drag, isActive }) => {
    return (
      <View>
        <DailyTodo item={item} drag={drag} isActive={isActive} />
      </View>
    );
  };

 

그리고 현재 프론트 레포에서 상태관리는, post/patch/delete API로 상태값에 변화가 생길 경우, 해당 API로 요청을 보내고 응답을 정상적으로 처리한 다음, 다시 get API로 변화된 값을 받아오는 방식으로 하고 있다. 그런데 이 방식이 데이터가 많아지거나 post/patch/delete 요청이 빈번해질 경우에는 문제가 될 수도 있어서, 추후 멘토링 때 이 부분을 멘토님께 여쭤보면 좋을 것 같다. 

 

아무튼 남은 이슈는 이 get API로 새로운 상태값을 받아올 때, 현재 투두의 값에는 날짜 및 카테고리로 필터링된 상태만 남겨야 하는데, 이 로직을 react query로 만들어서 get API를 호출할 때마다 이 과정이 자동으로 일어나도록 만드는 것이다. 


우선은 todos 렌더링까지는 되었으니, 이제 subtodo를 만드는 것이 잘 되는지를 우선 확인하려고 했다. 그런데 이런 에러가 뜬다. 

 

DailySubTodo라는 하위 투두 컴포넌트로 데이터가 제대로 전달되지 않는 것 같다. 이 문제는 괄호{}를 추가했다 빼면서 여러 방면으로 삽질(...)을 했더니 잘 해결되었다. 

하위투두와 투두가 잘 나타난다

 

이제 문제는 투두 모달창에서 '하위 투두 생성하기'를 누르면 이렇게 텍스트 인풋창이 생기는데, 이 인풋창이 자동으로 활성화되지 않는다는 거였다. GPT에게 물어보니 useRef() 이라는 리액트 훅을 사용해서 해당 텍스트 인풋창을 참조해 놓고, focus(활성화) 시켜야 할 일이 있으면 그때 활성화를 시키는 방식으로 하라고 조언해주었다. 

 

그런데 이렇게 하려면 텍스트 인풋 컴포넌트와 그 컴포넌트를 활성화시키는 버튼이 같은 파일에 있어야 하는데 그렇지가 않았다. 그래서 급한 대로 autoFocus={true} 라는 코드를 추가해 주었다. 그랬더니 처음에 활성화 시키는 부분은 잘 되었다. 그런데 다시 모달을 클릭해서 활성화시키려니 바로 focus가 없어져 버렸다. 일단은 특정 기능을 불가능하게 하는 큰 요인은 아니니 넘어가기로 하였다...

 

그리고 팀원들과 프론트 코드를 보면서 논의를 거친 끝에 카테고리 및 날짜 별로 필터링하는 태스크와, 하루에 있는 투두의 숫자를 나타내주는 태스크가 완료되었다!

 

 오늘 배운 것

프론트 레포에서 팀원의 react query 관련 PR을 리뷰하는데 PR의 규모가 심상치 않았다. 장기적으로 꼭 필요한 리팩토링이고, 나도 이 리팩토링 코드를 받아서 앞으로의 작업을 이어가야 하는 상황이라 어제 했던 캘린더 모달 작업을 계속하려면 이 React Query에 대한 이해가 필요할 것 같다. 

 

해당 PR과 관련된 올려둔 브랜치를 pull 받아서 확인해보니 401 에러가 떴다. 일단 서버가 잘 살아있긴 한데, 메모리나 CPU 사용량에서 무리하고 있지는 않은지도 확인해 보고, 서버에서 띄우는 에러 메시지가 있는지도 확인해 보았다. 

일단 서버는 살아있긴 하다

 

그리고 ECS 태스크의 로그를 보니 띄워진 WAS에서 무언가 에러를 내고 있었다... 정확한 원인은 파악해봐야 알겠지만 프론트에서 내린 요청과 서버의 API 메소드가 일치하지 않아서 나는 오류이지 않을까 싶다. 다시 보니 몇 분 사이에 서버의 로그가 몇백 개가 늘어나 있었다. 프론트 react query에서도 요청을 계속 보내고, 서버에서도 계속 에러 코드 500을 리턴하면서 그 사이에 로그가 늘어난 것 같았다. 

 

일단 원격 서버에서 연결해서 콘솔에서 에러 로그를 보면서 디버깅하는것도 좋지만, 어차피 원격 서버에 띄워진 WAS의 상태는 main 브랜치에 올라온 코드와 완벽히 동일하기 때문에(깃허브 액션을 설정한 보람이 있다) 로그를 보면서 작업하기에는 로컬에 서버를 띄워두고, 팀원이 config.js 파일에 명시해둔 URL을 잠시 로컬 서버로 돌려두고 작업하는 것이 더 편할 것 같았다. 

 

암튼 이렇게 세팅해 두고 소셜로그인 버튼을 누르니, 서버에서는 401 에러가 났다. 로컬 백엔드 서버의 로그를 확인해보니 user_id는 undefined가 아니라 int여야 한다... 식의 에러라서 아마 파라미터 전달이 잘못된 것 같다. 이 부분을 수정해 주면 될 것 같아서 수정해 주었다. 알고보니 ({}) 형식으로 파라미터를 받던 코드를 그냥 ()로 수정해 주었더니 파라미터 바인딩이 잘 되더라. 

 

이제 오늘 작업하려던 SZ-118 이슈의 투두 모달에서 캘린더를 통해 투두 날짜를 변경할 때 API를 호출하는 로직을 짜면 된다. 사실 어제 작성해 뒀었는데, 해당 로직은 fetch()를 써서 작성된 것이나 이제부터는 API를 호출할 때 react query를 사용하기로 했기 때문에 로직을 다시 작성해야 한다. 문제는 내가 react query를 하나도 모른다는 것...! 급한 대로 다른 팀원이 작성한 코드를 슥 보고 어떤 식으로 사용하는지만 일단 익혀보자. 

 

그리고 하다보니 또 다른 문제가 생겼다. 무엇이냐면 내 아이디로 작성된 투두가 하나도 없다는 거였다. 그럼 투두를 작성하면 되지 않나? 라고 생각할 수 있지만, 팀원들 서로 맡은 부분이 다르기 때문에 현재 다른 팀원은 투두 카테고리 관련된 부분을 작업 중이고, 나는 투두 모달 부분을 작업 중이었다. 그런데 투두 모달을 보려면 일단 만들어진 투두가 필요하다. 그리고 투두 만드는 부분에 대해서는 다행히 react query로 기초 작업이 되어있었지만 실제로 텍스트 인풋에 내용을 입력하고 엔터를 누르는데 투두가 안 만들어진다. 

 

즉 투두 모달창에서 react query로 API 호출 로직을 짜려면 그 전에 react query로 투두 만드는 API가 잘 호출되어야 하고, 이 작업이 아직 되어있지 않아서 dependency 이슈가 생겼다. 그러므로 이걸 먼저 완료하고 투두 모달 작업을 마저 하는 게 맞겠다고 생각되니 까먹지 않도록 지라 이슈를 편집해두자. 

 

오늘 해야 할 일들이다. 

 

프론트에서 react query, axios를 이용해서 투두 생성하는 API에 요청을 보내고 있는데, 서버에서는 파라미터가 없다고 뜬 오류가 있었다. 알고보니 API request에 쓰일 헤더를 정의하는 함수에서 {} 괄호를 없애 주었더니 파라미터 바인딩이 잘 되었다. 이제는 투두 날짜수정 API를 react query를 통해서 호출하면 된다! (이 과정도 1시간이나 걸렸다...)

 

그런데 카테고리를 post하는 API를 호출했을 때는 {} 괄호를 없애면 되었는데, 문제는 유저 데이터를 retrieve하는 API는 괄호를 없애니까 에러가 난다. 즉 두 개의 API 호출 로직에서 무언가가 다르다는 말이었다. 하지만 생각해보니 일단 앞의 시행착오를 통해 무사히 내 유저의 category는 잘 생성하였고, 그러면 이 문제는 일단 두고 내가 맡은 투두 모달 이슈를 먼저 하는 게 맞겠다는 생각이 들었다. 

 

투두 모달 이슈를 처리하기 위해서는 또 나름의 문제가 있었다. 바로 투두를 생성할 때, selectedCategory라는 상태 변수에서 유저의 카테고리 중 가장 첫 번째(맨 처음에 뜨는 화면에서 선택되어 있는 카테고리)의 값을 갖고 있어야 했다. 그러기 위해서는 이 selectedCategory의 값을 받아오는 CategoryContext 내부에서 맨 처음에 null로 초기화되었던 이 selectedCategory의 값을, category API를 호출해서 그중 맨 첫 번째 category의 ID 값으로 바꿔줘야 했다. 

 

원래 계획은 기존에 hooks 폴더에 별도로 정의된 useCategoriesQuery() 라는 react query를 통해 category 데이터를 불러오는 함수가 있어서, 이 함수를 useEffect() 안에 선언해서 사용하려고 했다. 그런데 useCategoriesQuery는 react hook이기 때문에 컴포넌트나 커스텀 훅 함수 안에서 말고는 사용할 수 없다는 메시지가 떴다. 그래서 다른 팀원이 이를 사용하고 있는 코드를 참고해서 작성하였다. 

const { isLoading, error, data, isSuccess } = useCategoriesQuery(
	accessToken,
	userId,
);

useEffect(() => {
	if (isSuccess) {
		setSelectedCategory(data[0].id);
	}
}, [data, isSuccess]);

 

이런 식으로 해당 훅 함수를 호출하면 isSuccess라는 변수를 받을 수 있다. 이 변수는 해당 훅이 성공적으로 실행되었을 때 true를 반환하며, 이를 통해 useEffect() 안에서 해당 훅이 성공적으로 실행되었을 때 selectedCategory 변수에 값을 할당할 수 있다. 왜 이런 식으로 리액트 훅을 동작하게 만들었는지는 잘 모르겠다..! 나름의 이유가 있을 텐데 말이다. 

 

그리고 투두를 생성하는 데도 문제가 있었다. 아까와 같이, react query를 사용해서 만든 커스텀 훅이 파라미터를 제대로 전달하지 못하는 오류였다. GPT에게 물어봤더니 이를 위해서 async를 사용하는 비동기 커스텀 훅을 만들고, 그 훅 함수를 useEffect() 내부에서 사용하는 방법을 제시해 주었는데... 일단 이해하기 너무 복잡했다. 그래서 임시로 postman으로 투두를 생성하는 API를 호출하고, 그 임시 데이터를 통해 모달작업을 해 보기로 했다. 

 

그랬는데 또 다른 문제가 생겼다... 위 방법을 통해 투두는 잘 생성되었지만, 그 투두 데이터가 앱에 나타나지 않는 거였다. 왜냐하면 상태관리 설정이 제대로 안 되어있기 때문이다. 그러니까 이 상태관리 설정들이 다 완료된 다음에야 생성된 투두 데이터가 제대로 나타날 수 있고, 그 투두 데이터를 클릭한 다음에야 모달이 활성화되어서 원하는 작업을 이어할 수 있는데, 이 dependency를 가진 이슈들이 아직 완료되지 않은 상황이다. 

 

아 그리고 위에서 CPU랑 메모리 사용량 그래프를 보다가 문득 '저 정도면 CPU랑 메모리 사용량이 안전한 걸까?' 라는 생각이 들어서 멘토님께 여쭤보았다. 멘토님께서는 그거는 SLI랑 SLO를 정의하기 나름이라고 하셨다. 서비스가 어느 정도의 성능과 사용량을 추구할 것인지에 대한 지표인 것 같았다. 


그리고 인프런 강의를 들으면서 ELB(elastic load balancer)에 대해서도 배웠는데, ELB의 세부 종류 중 ALB(application LB)와 NLB(network LB)의 차이가 무엇인지가 궁금해졌다. 둘 다 여러 개의 서버에 가해지는 부하(로드)를 밸런싱해준다는 것은 잘 알겠는데, ALB는 어플리케이션 계층에서, NLB는 송신(transport) 계층에서 동작한다는 말이 정확히 무슨 의미인지 와닿지 않았다. 

 

아니면 요청이 low layer에서 high layer로 올 때, 말 그대로 ALB는 7계층에서 동작하고 NLB는 4계층에서 동작한다는 것인가? 의미로는 알 것 같은데 그러면 7계층에서 로드밸런싱을 하나 4계층에서 로드밸런싱을 하나 구체적으로 무슨 차이인지 잘 와닿지 않는다. 

 

그리고 'ELB를 사용할 때 EC2 인스턴스는 private address 밖에 알 수가 없다. 그러므로 x-forwarded-for' 헤더를 사용해서 public address 값을 알아낼 수 있다' 라는 내용이 잘 이해가 되지 않는다. 그렇다면 EC2 인스턴스 설정에 나오는 public endpoint는 다른 의미인 것일까? 그 차이를 잘 모르겠다. 

 

그리고 EC2와 관련이 있는 Route53 서비스도 나왔다. 이 친구는 AWS에서 제공하는 DNS 서비스라고 하며, EC2 인스턴스나 S3 버킷, 로드밸런서에 public DNS를 제공해주는 역할을 해 준다고 한다. 도메인 주소를 구매하여 연결시켜 준다고 하니, 우리 상황에 딱 맞다. 

 

 오늘 배운 것

앞서 서버 배포 관련해서 급한 작업을 얼추 완료하고 다시 프론트로 넘어왔다. 현재 해야 할 일들 중에 마지막 하위 이슈의 경우는 지금 다른 팀원이 react query랑 기타 중복된 로직들을 바꾸고 있기 때문에 내가 작업하면 코드가 중복되거나 꼬일 수도 있다는 생각이 들었다. 그래서 리스트들 중에 1, 2, 3번은 모달창 안에서만 상태관리를 하면 되기 때문에 얼른 미리 작업해 두기로 했다. 

 

우리 서비스에서는 MVP를 앱으로 만들기로 해서 프론트를 앱개발로 하고, 프레임워크는 RN(react native)을 사용한다. 그리고 다들 개발자라 컴포넌트 디자인에 많은 시간을 쏟지 않기 위해서 디자인 라이브러리도 사용한다. 여러 선택지들이 있었지만 투표를 통해 UI kitten을 사용하게 되었다. 그리고 정말 다행히도 UI Kitten에서는 캘린더 컴포넌트를 기본으로 제공해서 직접 구현하지 않아도 된다. 

 

오늘 할 일은 여기서 제공하는 캘린더 컴포넌트를 사용해서 모달창을 열었을 때 '투두 날짜 옮기기' 버튼을 만들고, 그 버튼을 누르면 아래에 캘린더가 뜨게 하고, 선택한 날짜로 API를 호출하는 것이다. 

 

구현되기 전 모달창은 아래와 같다. 이 '하위 투두 생성하기' 버튼 밑에다가 '날짜 바꾸기' 버튼을 추가할 것이다. 생각해보니 '보관함에 넣기' 버튼도 필요하겠네? 그럼 버튼을 두개 만들자. 

before
after

 

버튼을 만드는 과정은 쉬운데 어려운 것은 그 다음부터다. '날짜 바꾸기' 버튼을 누르면 기존의 모달창이 캘린더 모달창으로 대체되고, 그 안에서 날짜를 보고 선택할 수 있어야 한다. 모달창을 대체하는 것이 좀 까다로워 보여서 GPT에게 물어보았다. GPT는 대체로 100% 정확한 코드를 작성해 주지는 않더라도 80% 정도는 원하는 기능에 근접한 코드를 작성해 주기 때문에, 종종 '아 이거 어떻게 코드 작성하지' 라는 생각이 들 때 이런 식으로 구체적인 질문을 하면 제법 도움이 되었던 것 같다. 

 

그런데 GPT가 제시한 코드가 영 맞지 않았다. 분명 모달이 2개이기 때문에 Modal 컴포넌트나 관련 상태 변수가 2개 이상 등장해야 하는데 하나만 등장하는 것이다. 이 녀석이 제대로 이해하지 못했다고 생각해서 더 짧게 다시 질문해 보았더니, 이번에는 Modal 상태가 2개인 코드를 잘 제시해 주었다. 

 

GPT에서 제시한 모달을 대체하는 원리는 간단했다. 루트 컴포넌트 아래에 Modal 컴포넌트 두 개를 나란히 놓는다. 그리고 Modal이 보일지 안 보일지를 제어하는 상태 변수와 함수를 useState()를 통해 두 개씩 만든다. 

 

그러니까 이런 식이다.

const [modalVisible, setModalVisible] = useState(false);	// 첫 번째 모달이 보이는지를 결정
const [calendarDate, setCalendarDate] = useState(new Date());	// 캘린더에서 선택한 날짜 변수
const [calendarModalVisible, setCalendarModalVisible] = useState(false);	// 두 번째 모달이 보이는지를 결정

return (
<>
      <Modal
        visible={visible}
        backdropStyle={styles.backdrop}
        onBackdropPress={() => {
          setVisible(false);
        }}
      >
      // 첫 번째 모달 내부의 컴포넌트
      </Modal>
      <Modal
        visible={calendarModalVisible}
        backdropStyle={styles.backdrop}
        onBackdropPress={() => {
          setCalendarModalVisible(false);
        }}
        style={styles.modal_calendar}
      >
      // 두 번째 모달인 캘린더 모달
        <Card disabled={true} style={styles.card_calendar}>
          <Calendar
            date={calendarDate}
            onSelect={nextDate => setCalendarDate(nextDate)}
            style={styles.calendar}
          />
        </Card>
        <Button>
          <Text>확인</Text>
        </Button>
      </Modal>
    </>
)

 

기본 상태는 두 모달 모두 안 보이는 상태이다. 그러다가 첫 번째 모달이 나타나고, '날짜 바꾸기' 버튼을 눌렀을 때 첫 번째 모달을 disable 시키고 두 번째 모달을 enable 시킨다. 이때 꼭 첫 번째 모달이 보이는지 결정하는 변수를 false로 바꾼 다음에 두 번째 모달이 보이는지 결정하는 변수를 true로 바꿔 준다. 

불편

 

그러면 두 번째 모달로 이런 캘린더가 뜬다. 그런데 중앙정렬이 좀 덜 되어서 미관상 부자연스러우므로, 이 문제만 해결하면 될 것 같다. 해당 모달은 모달>카드>캘린더의 nested 구조로 되어 있어서, 셋 모두에 스타일로 width: 100%를 적용해 주었더니 아래와 같이 중앙 정렬 문제가 해결되었다. 

편안

 

이제는 캘린더에서 선택한 날짜를 투두 수정 API의 파라미터로 전달해 주면 된다. 그런데 선택한 날짜 값을 찍어보니 현재 선택된 날짜보다 9시간 느린 시간이 떴다. 저번에 다른 팀원이 언급했던 timezone 문제로 보였다. 

 

GPT에게 질문해 보니 그냥 GMT 기준으로 나오는 시간에 9시간을 더해 KST로 만들라는 방법을 알려주었다. 물론 캘린더의 컴포넌트를 더 파고 들어가서 기본 타임존을 KST로 바꾸는 방법도 분명히 있겠지만, 해당 방법이 더 금방 걸릴 것 같아서 이 방법을 사용하였다. 

 

'개발 일기장 > SWM Onestep' 카테고리의 다른 글

20240801 TIL  (0) 2024.08.01
20240731 TIL  (0) 2024.07.31
20240729 TIL: ECR ECS CI/CD 적용기 with fargate & github action  (1) 2024.07.29
20240728 TIL  (2) 2024.07.28
20240727 TIL: ECR ECS 적용기  (0) 2024.07.27

+ Recent posts