✅ 오늘 배운 것
어제부터 이어진 시도가 계속되고 있다. 그냥 useMetadata()라는 커스텀 훅을 사용해서 헤더에 최신 토큰값을 반영하고 싶었을 뿐인데, 생각보다 많은 양의 코드를 건드리게 되었다.
다행히 vscode 컴파일러(표현이 맞을지 모르겠다)에서는 리액트 커스텀 훅을 부적절하게 사용하면 바로 문법 에러를 내 주었다. 이게 아니었으면 나의 삽질은 더 길어졌을 것이다.
일단 초기 코드는 다음과 같다. 어제 언급한 1, 2, 3단계의 방법으로 레포에 있는 모든 코드를 바꿔주었다.
const handleLocalToken = useCallback(useHandleLocalToken, [
setAccessToken,
setUserId,
useVerifyToken,
]);
const useHandleLocalToken = async () => {
const token = await getAccessTokenFromLocal();
const user = await getUserInfoFromLocal();
useVerifyToken(token)
.then(() => {
setAccessToken(token);
setUserId(user.userId);
router.replace('(tabs)');
})
.catch(e => {
router.replace('/');
});
};
useEffect(() => {
handleLocalToken();
});
그리고 문법 에러가 나지 않는 것을 확인하고 앱을 시작해 봤더니, 이런 오류가 났다.
useCallback 콜백 함수로 감싼 handleLocalToken 함수가 undefined 값으로 나오고 있었다.
알고보니 useCallback으로 감싼 handleLocalToken 함수를 useHandleLocalToken 커스텀 훅보다 먼저 정의해서, 즉 useHandleLocalToken의 값이 undefined일 때 해당 값을 가져오려는 handleLocalToken 함수를 사용해서 에러가 난 것이었다. 이 문제는 useHandleLocalToken 함수를 handleLocalToken 콜백함수보다 먼저 정의해 주니 해결되었다.
이제는 로그인 화면까지는 잘 띄워졌다. 그런데 그 다음엔 구글로그인을 하면 다시 TodayView, 즉 투두 리스트 뷰를 보여줘야 하는데, 로그인 화면에서 넘어가지를 않았다. 이번에는 비슷한 문제로 'useVerifyToken'과 'useGoogleLogin'의 값이 undefined로 뜨는 것 같았다.
즉 useVerifyToken과 useGoogleLogin 함수(커스텀 훅)를 실행시켜야 하는데, 이 값이 제대로 인식되지 않아서 함수를 실행하지 못하고, 그러므로 로그인이 되지 않은 상태로 간주되니 투두 리스트 뷰로 넘어가지 못하는 상황으로 이해했다. 이 부분은 알고보니 import 구문에서 오류가 있어서, 이 부분을 수정하니 해결되었다.
문제는 여전히 구글로그인을 하고 나서 투데이 뷰로 넘어가지 않는다는 점이었다. 'Invalid hook call'이라는 부분으로 보아, 어제와 같이 커스텀 훅을 사용하는 과정에서 오류가 있는 것으로 보았다.
GPT에게 언어추론 문제를 내듯이 질문을 여러 차례 해 보았지만 마땅한 해답이 없었다. 즉 내가 리액트 훅을 잘못 사용하고 있는 게 거의 확실한데, 그게 어떤 지점인지를 모르는 게 문제였다.
에러 메시지에서 올려준 공식문서의 링크를 타고 들어가보았다. 요약하면 리액트 훅을 적재적소에 잘 사용하라는 것이고, 반복문이나 조건문 안에 사용할 수 없다는 것이었다. 혹시나 vscode에서 훅의 잘못된 사용을 캐치하지 못한 부분이 있을까 싶어 공식문서에서 추천한 플러그인도 설치해 주었다.
그렇게 삽질을 반복하던 중, 문득 어제 결론으로 내렸던 1번-2번-3번의 방식이 문법적인 오류를 내지는 않을지언정, 리액트 훅을 올바르게 사용하는 방식은 아닐 것 같다는 결론을 내렸다. 왜냐하면 애초에 useCallback 훅은 리렌더링 사이에서 함수를 계속 렌더링하는 것을 막는 용도였지 커스텀 훅의 사용과는 무관한 훅이었기 때문이다. 또한 임시로 만들어둔 'check local token'이라는 버튼을 누르면 토큰을 인증하는 verifyToken API를 호출하도록 되어 있고, 버튼을 누르면 onPress 함수로 useCallback()을 사용한 'handleLocalToken'이라는 함수를 호출하도록 되어 있다.
그러니까 이런 셈이다.
const handleLocalToken = useCallback(useHandleLocalToken, [
setAccessToken,
setUserId,
useVerifyToken,
]);
//
<Button onPress={() => handleLocalToken()}>Check Local Token</Button>
즉 이 버튼을 누르면 바로 handleLocalToken이라는 함수가 실행되어야 하는데, 여기서도 'Invalid hook call'이라는 오류가 떴다. 그러므로 애초에 이 방식이 올바른 방식이 아니라는 결론을 내렸다. 다시 원점으로 가 보자.
그럴 듯한 방법을 찾았다. 요청을 처리하는 함수 자체를 훅으로 만든 다음, 그 함수를 커스텀 훅이나 컴포넌트에서 사용하도록 하는 방법이었다. 그런데 이걸 사용하는 방법에서도 문제가 있었다. 단순히 컴포넌트 최상단에서 훅을 사용하기만 하면 문제가 없는데, 이 훅을 컴포넌트에 있는 함수에서 사용하게 되면 '커스텀 훅은 일반 함수에서 사용할 수 없다'는 오류가 났다.
그러던 중 이런 오류를 발견했다.
몇번 더 삽질하면서 다음과 같은 질문을 해 보니, 커스텀 훅을 만들어서 사용할 때 내가 몰랐던 점에 대해서도 새롭게 알 수 있었다.
딱 이런 상황이었다. GPT 피셜, 리액트의 (커스텀) 훅은 컴포넌트나 다른 훅의 최상위에서 호출되어야 하기에 컴포넌트 내에서 커스텀 훅을 정의하고 그걸 Button의 콜백함수에서 호출하는 것 자체는 훅의 규칙을 위반한 것이라고 한다. 훅의 사용규칙을 설명하는 공식문서를 찾아보니 같은 내용이 있었다.
그렇다면 지금 작성된 코드를 다시 바꿔줘야 하겠다. 이쯤 되니 커스텀 훅에 대한 지식이 부족하다는 것이 많이 실감되었다... 커스텀 훅 관련 공식문서도 한번 읽어보자.
새롭게 정의된 문제 상황은 다음과 같다.
const useHandleLocalToken = async () => {
const token = await getAccessTokenFromLocal();
const user = await getUserInfoFromLocal();
const { useVerifyToken } = useApi();
useVerifyToken(token).then(() => {
setAccessToken(token);
setUserId(user.userId);
router.replace('(tabs)');
});
};
'Login' 컴포넌트 안에서 다음과 같은 'useHandleLocalToken' 이라는 커스텀 훅을 정의해두었다. 그리고 Button 컴포넌트의 onPress 함수로 해당 콜백함수가 호출되도록 하고 싶다.
무슨 문제인지 감이 잡히지 않아서 멘토님께도 도움을 요청드렸다. 멘토님께서는 useVerifyToken을 사용할 때 token만 파라미터로 넘기지 말고 onSuccess라는 콜백함수도 같이 넘기는 형태로 바꿔 보라고 말씀해주셨다. 처음에는 무슨 의미인지 잘 이해가 안 되었는데, GPT를 거친 다음 예제 코드를 보고서 이 피드백이 무슨 의미였는지 더 정확히 이해가 되었다.
그러니까 위의 코드를 이런 식으로 바꾸는 것이었다.
// index.jsx
const useHandleLocalToken = async () => {
const token = await getAccessTokenFromLocal();
const user = await getUserInfoFromLocal();
const { useVerifyToken } = useApi();
useVerifyToken(token, () => {
setAccessToken(token);
setUserId(user.userId);
router.replace('(tabs)');
});
};
// useApi.js
const useVerifyToken = async (token, onSuccess) => {
return useHandleRequest(() => axios.post(API_PATH.verify, { token }))
.then(response => {
onSuccess(response);
})
.catch(err => {
Sentry.captureException(err);
});
};
onSuccess 함수(함수가 성공적으로 수행되었을 시 호출하려는 함수) 하나를 인자로 받았을 뿐인데, 이렇게 하면 비동기 작업을 더 유연하게 처리할 수 있겠다는 생각이 든다..! 다만 아직 내가 비동기 함수 호출이나 콜백 등을 깊게 이해한 상태는 아닌 것 같다. 왜 저렇게만 바뀌었는데도 비동기 작업을 유연하게 처리할 수 있는 것이고, 저 상황에서 어떻게 하면 저런 해결책을 떠올릴 수 있을까? 라는 궁금증이 생긴다.
그리고 이렇게 바뀌었어도 useHandleLocalToken 함수는 여전히 커스텀 훅 함수라서 Button 컴포넌트의 onPress 속성으로 전달할 수는 없다. 분명 멘토님이 주신 피드백에 실마리나 힌트가 있었을 것 같은데, 아직 나의 얕은 프론트 지식으론 한 번에 캐치할 수 없었던 것 같다..! 일단 수정된 코드가 담긴 링크를 전달드리고, 위 부분에 대해서 다시 여쭤봐야 할 것 같다.
프론트 이슈가 잠시 막혔으니 백엔드로 Context Switching을 해 보자. 사실 프론트가 막혀서 그렇지 백엔드도 할 게 정말 많다! 이번 스프린트 때 내가 맡은 이슈들 중 백엔드 건은 크게 두 가지이다.
1. 백엔드 API 서버 부하 테스트
2. EC2 AutoScaling 적용
당장 모레 멘토링이 있는데, 이때 전까지 프론트 막힌 부분과 이 두 가지의 이슈를 모두 해결하는 것을 목표로 잡아보자. 그래야 멘토링을 할 때 더 의미 있는 피드백을 받을 수 있을 것 같다. 그리고 그래야 남은 기간동안 예상치 못한 오류도 고치고 중간평가 준비도 할 수 있다...
암튼 백엔드 API 서버 부하 테스트 이슈부터 처리해 보자.
GPT와 구글링에서 찾은 유용한 블로그 글을 참고하며 진행해 보겠다. 우선 장고 API 서버에 부하 테스트를 하기 위해서는 "부하 테스트 도구"를 뭘로 할지를 결정해야 한다. 블로그와 GPT에서는 모두 Locust를 선택했는데, 이유는 테스트 스크립트를 파이썬으로 작성할 수 있다는 장점이 있어서였다. 그렇다면 Locust를 사용해서 테스트를 해 보자. 필요하면 공식문서와 깃허브도 참고해 보자.
시작하기 전에 궁금증이 생겼다. 예를 들면 투두를 생성하는 API에 대해 부하 테스트를 하면 투두가 생성된다. 그러면 그 데이터들은 어디에 저장되어야 할까? 테스트 DB에 저장되는 게 맞아 보이는데, 그러면 테스트 DB와 별도로 연결하는 작업이 필요하지 않을까? 이런 의문이 들었다.
그렇지만 아직 Locust가 '부하 테스트 도구'라는 것 외에는 아무것도 이해하지 못했으므로... 일단 시작해 보자. Locust는 서드파티 라이브러리이기 때문에 pip로 간단히 설치할 수 있다.
pip install locust
그리고 서버의 accounts 디렉토리 안에 locustfile.py 파일을 하나 만들어 두고, 가장 간단한 API를 테스트하는 로직을 작성해 보았다. 해당 API는 서버가 살아있는지 테스트용으로 만든 API로, 'hello world'라는 응답을 주는 것이 전부이다.
from locust import HttpUser, task
class HelloWorldUser(HttpUser):
@task
def hello_world(self):
self.client.get("/todos/todo?user_id=1")
공식문서의 QuickStart 부분을 보니 'locust' 명령어로 Locust 서버를 시작시키고(표현이 맞는지 모르겠다) localhost:8089로 접속하면 Locust의 WEB GUI를 볼 수 있다고 한다. 이때 주의할 점은 단일 파일로 부하 테스트를 시작하고 싶다면 파일의 이름을 locustfile.py로 하고 루트 디렉토리 바로 아래에 위치시켜야 라이브러리에서 인식할 수 있다.
웹 GUI도 잘 뜬다!! 너무 신기하다... 암튼!
이제 host를 개발서버 엔드포인트로 지정해 주고 요청을 보내 보았다. 사실 해당 필드값이 정확히 뭘 의미하는지, 얼마나 설정해야 유의미할지를 몰라서 그냥 둘다 1로 설정하고 테스트가 되는지만 보기로 했다. 그러니까 지금은 1명의 유저가 요청을 보내는 상황에 대한 테스트가 될 것이다.
GET으로 투두를 조회하는 API를 호출해 보았다. RPS는 Request Per Second로 서버가 현재 1초에 얼만큼의 요청을 처리하고 있는지를 나타내는 것이라고 이해했다. 요청의 95%는 110ms 이내에, 99%는 140ms 이내에 서버에서 응답이 리턴되는 것이라고 이해했다. 다만 이 경우는 유저에 맞는 투두 리스트를 단순 조회하는 API라서 더 복잡한 API는 별도의 테스트 로직을 작성해봐야 하겠다.
✅ 궁금한 점
1. 위에서 정의한 useHandleLocalToken 함수를 어떻게 Button의 onPress 이벤트가 발생했을 때 실행되도록 할 수 있을까?
2. RPS가 높을수록, 그리고 ms 값이 적을수록 서버가 요청을 잘 처리한다는 것은 알겠다. 우리 서비스의 경우는 어느 정도의 값을 목표로 하면 좋을까? 그리고 서버의 성능을 증가시키려면 어떤 작업이 필요할지도 궁금하다.
'개발 일기장 > SWM Onestep' 카테고리의 다른 글
20240815 TIL: uvicorn+gunicorn으로 서버 성능 향상시키기 [진행중] (0) | 2024.08.15 |
---|---|
20240814 TIL: 리액트에서 커스텀 훅 올바르게 사용하기 & locust 부하 테스트를 위한 테스트 환경 설정하기 (0) | 2024.08.14 |
20240812 TIL: 개발 환경 분리 & react custom hook 만들기 (0) | 2024.08.12 |
20240811 TIL: github workflow에서 최신 브랜치의 내용이 반영되지 않는 문제 수정 & 개발 환경 분리 (0) | 2024.08.11 |
20240810 TIL: github workflow에서 최신 브랜치의 내용이 반영되지 않는 문제 수정 [진행중] (0) | 2024.08.10 |