인프런 강의를 들으면서 내가 이해한 내용들과 궁금한 점들을 정리해 보았다.

(중간중간 이해되는 내용이 많아질 경우 계속 내용이 추가될 예정이다)

 

 웹 어플리케이션 서버(WAS)와 웹 서버(WS)의 차이점은 무엇일까

WS는 정적인 컨텐츠에 대한 요청을 처리하며, WAS는 웹 어플리케이션 로직을 통해 동적인 요청을 처리할 수 있다는 점이 가장 큰 차이이다. 그러나 요즘은 WS도 설정하기에 따라 꼭 정적 컨텐츠에 대한 요청만 처리하는 것은 아니라고 해서... 무 자르듯이 아주 정교하게 구분하기는 어렵다고 한다. 또한 예전 멘토링에서 말씀하신 것처럼 WAS 안에 WS가 있는 구조라고 한다. WS는 예를 들면 apache, nginx 등이 있고, WAS는 사용하는 프레임워크 별로 다양하다. 가령 django WAS, tomcat 등이 있다. 

 

그러면 WAS만 사용하면 되는 게 아닌가? 굳이 정적인 요청만 처리할 수 있는 WS는 왜 사용할까

요청별로 처리하는 데 드는 비용이 다르다. 가령 주문과 결제 로직을 사용하는 요청의 경우 처리하는 데 드는 시간과 드는 리소스가 더 크고, 반면 단순 이미지를 요청하는 경우는 리소스가 더 작다. 그런데 이 모든 요청을 WAS에게 요청할 경우 값싼 요청을 처리하는 데에도 똑같이 WAS의 리소스를 할당해야 하기 때문에, 이 경우 요청을 먼저 WS가 처리하게 하여서 정적 컨텐츠 요청의 경우는 WAS의 리소스를 아예 사용하지 않도록 할 수 있다. 

 

 WAS는 어떤 기능을 제공하는가

WAS의 큰 장점 중 하나는 서블릿을 지원한다는 것이다. 서블릿을 사용하게 되면 개발자는 비즈니스 로직에만 더 집중할 수 있다. 서블릿에서는 가령 요청이 들어왔을 때 그 요청에서 헤더를 분리하고, 요청의 경로를 확인하고, 파라미터들을 일일이 다 파싱하고... 등등의 작업을 해 준다. 만약 서블릿이 없다면 개발자는 비즈니스 로직 외에도 위의 작업에 필요한 로직을 다 만들어야 했을 것이다. 이럴 경우 작업 효율성이 떨어지고, 코드도 훨씬 더 복잡해졌을 수 있다. 

 

WAS에서는 서블릿을 지원한다. 요청이 오면 서블릿에서 그 요청을 먼저 처리해서, 위에서 언급한 url 경로를 확인하거나 파라미터 및 헤더를 파싱하는 등의 공통적이고 반복적인 작업을 먼저 처리한다. 그 다음 요청이나 응답을 뷰(MVC 패턴을 쓰는 경우)에서 처리하기 쉽게 요청 및 응답 객체를 새로 생성하여 리턴해 준다. 이는 모든 웹 서버 프레임워크에서 공통적인 것으로 보인다. 지금까지 주로 사용해온 django의 경우도 마찬가지로, view 함수에서는 항상 request 라는 객체를 받았고, 그 안에서 request.headers 등의 기본으로 제공되는 메소드를 통해 요청의 헤더나 파라미터 값들을 쉽게 확인할 수 있었다. 

 

또한 WAS는 멀티스레딩 기능도 제공한다. 싱글 스레드로 클라이언트의 요청을 처리하게 될 경우, 앞선 요청이 지연되는 경우 뒤의 요청들까지 전부 밀리게 된다. 그렇다고 해서 요청이 들어올 때마다 스레드를 생성하게 된다면 스레드를 생성하는 비용이 비싸고, 그렇게 만든 스레드를 재사용할 수 없어서 비효율적이며, 스레드 간 context switching 비용이 발생하며, 요청을 무한대로 제한 없이 받을 수 있어 과도한 요청이 들어오면 WAS가 다운될 수 있다는 단점이 있다. 그래서 보통은 스레드 여러 개를 모아 둔 스레드 풀을 만들어 두고, 요청이 들어오면 스레드 풀에서 필요한 스레드를 꺼내 쓰는 방식으로 사용한다. 

 

✅ 궁금한 점들

django WAS는 다른 WAS와는 어떻게 다를까. 서블릿은 어떻게 구현되어 있는지, 또 멀티스레딩은 파이썬에서 어떻게 구현했을지 궁금하다. 

톰캣에서 사용하는 스레드 풀의 기본 max thread 값은 200개라는데, 현재 개발하고 있는 서비스(onestep)의 경우는 그렇게 많은 스레드가 필요해 보이지는 않는다. 얼마 정도가 적당할지 궁금한데, 아마도 테스트를 통해서 알 수 있을 것 같다. 그러니 테스트도 해 보자.

 

'server-side > server' 카테고리의 다른 글

Mac 환경설정  (0) 2024.07.15
Software Release Life Cycle  (0) 2023.07.15
OAuth 2.0 기본원리  (0) 2022.09.26
인증(Authentication)  (0) 2022.07.14
linux: cron 사용해서 자동으로 스케줄 실행하기  (0) 2022.07.09

 오늘 배운 것

오늘은 어제 작업하다가 미처 처리하지 못한 인박스 뷰의 남은 이슈와, 프론트에서 다른 팀원들과 협업이나 논의가 필요한 일부 버그 빼고 나머지 기능들이 잘 동작하는지를 확인하는 것이 목표였다. 그리고 백엔드 이슈의 경우는 이제는 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

 오늘 배운 것

오늘 오후까지도 ECS 배포 이슈가 이어지고 있다. 어제 fargate 옵션을 통해 서버를 띄우는 데까지는 성공했다. 장고 서버가 무사히 시작했다는 로그도 보았다. 이 이후에 다른 문제가 있었다. 

어딘가에서 서버가 실행되었던 흔적

1. 어딘가에서 잘 실행되고 있는 이 서버에 어떤 엔드포인트로 접속해야 하는지 모르는 문제

2. 마이그레이션 명령어(python manage.py migrate)를 실행하지 않고 바로 실행시켰기 때문에, 분명 RDS에 마이그레이션이 적용되지 않았을 것이다...

-> 이렇게 생각했었는데, 사실 이미 우리 서비스는 원격 RDS를 사용하고 있다. 그렇다는 것은 이미 로컬에서 RDS 연결 테스트를 할 때 명령어로 마이그레이션을 실행해 주었으니, 사실상 이 문제에 대해서는 걱정하지 않아도 되겠다. 

 

어쨌든 1번 문제는 아직 남아있다. 게다가 방금 또 다른 문제 상황을 알게 되었다. 분명히 어제 밤까지는 서비스 내부의 태스크 로그에서 python manage.py runserver 명령어가 잘 실행된 로그를 확인했었다. 그런데 오늘 다시 확인해 보니 실행 중인 태스크가 없는 거였다..!

 

어제의 경험을 바탕으로 CloudFormation 서비스에 들어가 로그를 살펴보니, ECS에서 failure가 났다는 로그가 남아있었다. 

상세 사유를 확인해 보니, fargate 인스턴스에서 ECS 서비스를 생성하는 데 실패했다고 나온 것 같았다. 무슨 문제일지는 더 살펴봐야 알 것 같다. 

아무래도 기존에 'ECR ECS'로 검색하면서 나온 여러 블로그와 영상들 중, EC2가 아니라 fargate 옵션이라서 보지 않았던 자료들을 다시 참고해야 할 것 같다. 


fargate로 서버 띄워서 접속했다!!!

위의 상황에서 문제가 없는지(로드밸런서 구성은 잘 되었는지, 퍼블릭 서브넷에는 잘 연결되어있는지 기타등등)를 한번 더 점검하고 ECS 클러스터 서비스를 업데이트했다. 

 

역시나 위에처럼 서버는 또 내가 모르는 어딘가에서 잘 실행되는 것처럼 보였다. 사용한 로드밸런서의 퍼블릭 DNS에서도 접속이 안 되고, 태스크의 ENI에 나오는 퍼블릭 주소로도 접속이 안 되길래 도대체 뭐지 싶었다. (여기까지는 어제랑 똑같은 상황이다)

냉큼 도움요청

GPT에게 ENI와 로드밸런서의 퍼블릭 주소는 멀쩡한데 접속이 안 된다고 물어보니 보안그룹 설정을 확인해보라고 했고, ECS 태스크의 ENI에서 사용하고 있는 보안그룹 설정을 보니 Anywhere IPv4 설정이 안 되어있는 게 아닌가. 사실 이것도 안드로이드 엡에서 접속하는거면 분명 특정 도메인이나 IP나 프로토콜 접속만을 허용해야 하는 무언가의 룰이 있을 텐데... 일단 이것은 개발서버이니 전부 열어두었다. 

 

그랬더니 이렇게 잘 접속되었다. 

 

로그도 잘 찍히고 있나 싶어서 ECS>태스크>로그를 확인해보았다. 

Not Found는 기본 URI에 매핑된 API가 없어서 그런 것이고, 404 response를 리턴하는 이유는 왜인지 모르겠다. 

404라니 뭔가 찜찜

 

암튼 급한 불은 껐다. 

 

이제 이어서 해야 할 일은 github action으로 이 모든 과정을 자동화하는 yaml 파일을 만들고, 그 과정을 통해 컨테이너가 무사히 ECR>ECS까지 잘 등록되는지를 확인하는 것이다. 


평화와 성공의 기쁨도 잠시, 또 Timeout이 났다. 

 

GPT에게 질문해서 오류를 파악해 보았다. 

 

GPT가 제시한 방안은 다음과 같았다.

1. Fargate의 태스크가 중지되었을 수 있으니 이벤트 로그를 확인해라. 

2. 로드밸런서의 헬스 체크 설정에 문제가 있는지 확인해라. 

3. 리소스 부족 때문에 태스크가 중지된 건 아닌지 확인해라. 

4. 애플리케이션 자체의 오류일 수도 있다. 

5. 보안그룹이나 네트워크 ACL 같은 네트워크 설정에 문제가 있을 수 있다. 

 

1번의 경우 이벤트를 확인하니 에러가 났다는 로그가 있었다. 

 

2번의 경우 헬스 체크 설정에 문제가 있는 게 맞았다. 사용하고 있는 로드밸런서(onestep_alb) 설정에 들어가니, 1개의 인스턴스에서는 요청이 타임아웃 되었고, 다른 하나의 인스턴스에서는 헬스체크가 fail 되었다.

 

3번의 경우 CPU 사용량 등의 리소스 그래프가 확 튀는 모양은 없었으므로 이것은 아닌 것 같았다. 

4번의 경우 현재 접속한 것은 swagger API 뿐이고 별다른 오류 로그가 없었으므로 아닌 것 같다. 

5번의 경우 그게 원인이었다면 5분 뒤에 접속이 끊길 리 없으니 가능성이 낮다. 

 

오류의 원인이 1-2번이라면 오류가 설명이 된다. 

즉 로드밸런서의 헬스체크가 fail로 간주되기 전에 계속해서 시도를 하던 동안에는 서버 접속이 잘 되었는데, 로드밸런서의 헬스체크 기준이 잘못된 것 같았다. 그래서 헬스체크를 fail로 간주하고, 이 태스크가 실패한 것으로 간주되면서 서버에 접속이 안 된다고 추측하고 있다. 그렇다면 로드밸런서의 헬스체크 기준을 바꿔줘야 하겠다. 

 

그래서 바꿔줬다. 

정확히는 EC2의 로드밸런서에서 사용 중인 로드밸런서를 클릭하고, 이 로드밸런서가 어떤 대상 그룹으로 요청을 보내주는지를 확인했다. 

나의 경우 onestep_alb라는 로드밸런서가 onestep_dev라는 대상 그룹으로 요청을 보내준다. 

 

그런데 onestep_dev 대상그룹의 헬스 체크 기준을 보니 기본 URI(/)로 요청을 보내서 200을 받는 것이 헬스체크의 기준으로 되어있었다. 그런데 어플리케이션 서버의 경우 / 에는 맵핑된 URI가 없었기 때문에 404 에러가 나서 헬스체크에 실패했던 거였다. 그래서 이 URI를 다시 /swagger(모든 API 명세를 볼 수 있는 URI로 상태코드 200을 리턴한다)로 바꿔주었다. 만약 이게 문제였다면 이제는 성공할 것이고, 아니면 또 다른 원인을 찾아야 하겠다. 

 

일단 다시 서비스를 업데이트 하니 서버가 또 잘 뜨긴 한다. 이제 몇 분 뒤에 헬스체크가 다 되었을 시점에 다시 결과를 보면 되겠다. 

 

헬스체크 규칙을 바꾸니 10분이 넘어가도 서버가 살아있다! 원인이 이것 때문이었던 것 같다. 그런데 나머지 1개의 인스턴스는 왜 timeout이 뜨는지는 잘 모르겠다. 

원래는 2개 다 fail이었는데 이제 하나는 정상이 뜬다.


로드밸런서가 계속해서 /swagger를 호출하면서 헬스체크를 계속하는 이유는 뭘까? 로그를 봤더니 거의 1000개가 있어서 놀랐다. 

찾아보니 로드밸런서의 헬스체크 기준에 원인이 있는 것 같았다. 헬스체크 기준은 로드밸런서가 가리키는 대상 그룹(target group)에서 정하는데, 현재 로드밸런서는 10초에 한 번씩 /swagger로 API 요청을 보내고 있었던 것이다. 이러니 부하가 있을 수 밖에! 그래서 10초 대신 maximum인 300초로 바꿔주었다.

 

그리고 깃허브액션으로 위 작업을 자동화하는 데 성공했다! 여기에는 GPT의 공이 컸지만, 그래도 나도 이 작업을 얼추 이해했기에 내 이해도 돕고 지식도 정리할 겸 남겨본다. yml 파일만 잘 작성되어 있다면 생각보다는 과정은 간단했다. 

 

우선 yml 파일을 GPT의 도움을 받아 작성해준다. 이 yml 파일이 제대로 동작하려면 그 전에 완료되어야 하는 사전 작업들이 있다. 

 

우선은 ECR에 도커 이미지를 올릴 수 있는 레포지토리가 있어야 하고, ECS 클러스터가 생성되어 있어야 하며, 그 클러스터 내에서 실행될 서비스가 생성되어 있어야 하고, ECS 클러스터에서 생성될 작업에 대한 정의(태스크 정의)가 작성되어 있어야 한다. 그리고 여기에 관련된 모든 변수들(ECR 레포지토리의 이름, ECS 클러스터의 이름, ECS 클러스터 안의 서비스의 이름 등)이 github secret에 변수로 등록되어 있어야 한다. 그래야 yml 파일에서 이 값들을 그대로 노출시키지 않고 변수를 통해 할당할 수 있다. 

 

ECS 클러스터에서 생성한 태스크 정의는 변수로 등록할 필요는 없다. 그럼에도 이 태스크 정의가 필요한 이유는 yml 파일을 작성할 때, ECS의 어떤 클러스터의 어떤 서비스에서 어떤 작업(태스크)를 실행할지를 명시해줘야 하는데, 이때 태스크 정의를 생성하면 나오는 JSON 파일이 필요하기 때문이다. 

 

사실 이 JSON 파일도 원래는 프로젝트 루트 디렉토리에 두려고 했었다. 그런데 JSON 파일에서는 .env 파일의 환경변수를 바로바로 등록해주지 못하고 별도로 파이썬 스크립트 파일을 하나 만들어서 그 파일에서 JSON 파일에 환경변수를 집어넣어주는 작업을 해야 한다. 굳이 싶어서 그냥 yml 파일에 그 작업을 넣어주기로 했다. 

 

그리고 GPT의 도움을 받아 생성된 yml 파일을 넣어주면, 잘 동작할 수도 있고 오류를 반환할 수도 있다! 나의 경우는 ECS 클러스터의 값이 예전에 생성된 값이었어서 해당 클러스터가 없다는 로그가 떴었다. 이처럼 깃허브 액션의 로그를 잘 보고, 알아서 잘 딱 깔끔하고 센스있게 GPT의 도움을 받아 고쳐주면 머지않아 이런 감동적인 화면을 볼 수 있다. 

 

궁금한 점

1. ENI는 또 어떤 녀석이며 왜 필요할까

2. 로드밸런서의 DNS 주소와, ECS 태스크의 ENI에 나오는 퍼블릭 주소는 각각 어떤 개념이며 무엇이 어떻게 다른가? 

3. 네트워크 ACL은 어떤 녀석인가

4. 로드밸런서에서 헬스체크가 fail이 나면 어떤 일이 발생하나? 설마 태스크의 실패로 간주되어 서비스가 종료되나?

5. 로드밸런서에서 대상그룹으로 요청을 보내준다(포워딩인 듯)는 개념이 어떤 의미일까? 이 개념이나 원리가 잘 이해가 안 된다. 

 

참고한 블로그

https://velog.io/@eunocode/%EC%A1%B0%EA%B0%81%EC%A1%B0%EA%B0%81-json-.env

 

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

20240731 TIL  (0) 2024.07.31
20240730 TIL: RN에서 UI Kitten으로 모달에서 캘린더 띄우기  (0) 2024.07.30
20240728 TIL  (2) 2024.07.28
20240727 TIL: ECR ECS 적용기  (0) 2024.07.27
20240726 TIL  (0) 2024.07.26

+ Recent posts