-
[Stack-Overflow 클론] 질문 작성 페이지 기능 구현Studying/Proj 과정 2023. 6. 21. 10:11
개요
스택오버플로우 클론 코딩 프로젝트에서 '질문 작성 페이지'를 구현하게 되었습니다.
TypeScript + react-query를 처음 써보는 기회가 되었고, 그만큼 급하게 배워서 사용한 탓에 라이브러리의 주 목적과 장점을 하나도 살리지 못 하는 결과가 발생했습니다. 따라서 잘못된 부분을 구체적으로 블로깅하고, 다음 프로젝트에선 그러지 않도록 하려고 합니다.
화면 UI
질문 작성 페이지 UI는 다음과 같습니다. (다른 팀원분께서 작업)
구현 계획
지금까지 동기, 비동기 데이터 모두 useState로 상태관리를 했습니다. 그러다보니 관리하는 데이터가 많아질 수록 상태값이 너무 많고 복잡해졌고, 코드가 보기 흉했습니다. 그 대책안이 없을까, 라는 생각만 늘 해오다가 이번 Pre-Project를 진행하면서 다른 팀원분께 react-query 라이브러리를 배우게 되었습니다.
클라이언트에서 바로 생성하는 정적인 데이터는 Redux 상태관리 라이브러리를 쓰고, 서버를 통해 받는 비동기 데이터는 react-query로 관리한다고 합니다. 그리하여 Pre-Project에서도 두 라이브러리를 통해 정적인 데이터와 비동기 데이터를 구분하는 방법을 채택하게 되었습니다.
제가 맡은 페이지인 <질문 작성 페이지>에는 서버에 POST 요청을 보내게 됩니다. 이 부분을 useMutation으로 구현해 보기로 했습니다.
더하여, Form 데이터도 react-hook-form을 활용하여 관리하기로 했습니다. 이 또한 처음으로 도전하여 어설픈 부분이 종종 있었던 것 같습니다.
react-hook-form으로 데이터 전달 받기
가장 먼저, react-hook-form 에서 사용할 훅(useForm)과 타입 정의에 필요한 두 가지 데이터 타입(SubmitHandler, FiledValues)를 불러왔습니다.
import { SubmitHandler, useForm, FieldValues } from 'react-hook-form';
useForm 훅을 사용해 다음 2가지 함수와 상태를 구조분해 할당으로 꺼내옵니다.
const { register, handleSubmit, formState: { isSubmitting } } = useForm();
(더 찾아보니까, 타입스크립트로 구현할 경우 데이터 타입들을 더 정확하게 <FormValue> 제네릭 타입을 명시하네요)
const { register, handleSubmit, formState: { isSubmitting } } = useForm<FormValue>();
각 함수에 대한 구체적인 사용법에 대해 차례대로 정리해보겠습니다.
register 함수는 react-hook-form 에서 가장 기본적인 기능을 제공합니다.
register 함수는 입력 받고자 하는 필드에 무조건 할당 해주어야 합니다. register 함수를 등록하면 입력된 값들이 form의 데이터로써 사용할 수 있게 됩니다.
<AskInput placeholder={element.placeholder} {...register('title')}/>
그런데 등록하는 방식이 조금 특이한 모양새입니다. 스프레드 문법을 사용하여 비구조 할당으로 함수를 등록하는 건 처음이었습니다. 그 이유가 무엇인가 하니, 바로 다음과 같은 것 때문이었습니다.
공식 문서에 따르면, register 함수는 다음과 같은 함수를 자동 생성합니다.
이렇게 생성된 함수를 통해 react-hook-form이 변화를 감지하고 필드에 입력된 값을 다양하게 쓸 수 있습니다.
이것들로 하여금 register를 등록했을 때 필드값을 가져오는 것 뿐만 아니라, 사용자 유효성 검증도 가능합니다.
(하지만 이번 Pre-Project 에서는 유효성 검증 우선순위를 하로 두어서 지금 당장 구현 과정을 적진 않습니다)
또한 저는 단순하게 register에 필드값 하나만 등록했는데, 아래와 같이 다양한 방법으로 필드값을 관리할 수 있습니다. 입력 필드가 카테고리별로 다양하다든지, 조금 복잡한 구조를 가졌다면 아래와 같이 Form 데이터를 정리하면 좋을 거 같습니다. 저도 저런 방식을 써야할 만큼 복잡한 Form을 구현해보고 싶다는 생각이 듭니다.
다음은 handleSubmit 함수입니다. 이름부터 제출 핸들러라는 걸 알 수 있습니다. 정상적으로 Form에 등록된 데이터는 handleSubmit 함수를 통해 받을 수 있습니다. 등록 방법은 form 태그의 onSubmit 속성에 핸들러를 전달해주면 됩니다.
<form onSubmit={handleSubmit(onSubmit)}> /* ... */ </form>
form 태그의 onSubmit 속성에 이벤트 함수를 바로 등록시키지 않고 handleSubmit으로 감싸주는 이유는, 이렇게 전달해야지만 태그 안에 있는 각 항목이 직접 작성한 조건(검증 등)에 맞는지 감지하고 이벤트를 제어하기 때문입니다.
가독성을 위해 onSubmit이라는 함수를 따로 정의해서 handleSubmit 함수에 전달해 주었습니다.
const onSubmit: SubmitHandler<FieldValues> = useCallback( (data:FieldValues) => { mutation.mutate(data, { onSettled:(data) => { window.scrollTo(0,0); navigate(`/questions/${data.questionId}`);; } }); }, [mutation] )
갑자기 mutation이 튀어나와버렸지만, 그냥 POST 요청을 받아온다고 생각하면 됩니다. 사실 Mutation에 대해서도 엉성하게 공부하고 바로 적용하여 조금 이상한 부분들이 보이지만 일단 form에 대해 적기 위해 지나치겠습니다.
실제적으로 Submit 기능을 작동시키는 건 아래 함수입니다. 해당 함수가 useMutation 훅에 전달되고, mutate 메서드를 통해 실행됩니다.
const addNewQuestion = (data:FieldValues) => { return call('/questions', 'POST', { memberId:isUser.memberId, title: data.title, content: data.problem + `\n\n` + data.expected, }); }
집중할 부분은 onSubmit: SubmitHandler<FieldValues> 입니다.
useMutation이랑 섞어 쓰느라 가독성이 별로 좋지 않은데, 단순하게 표현하자면 이벤트 핸들러는 다음과 같은 형태입니다.
const onSubmitHandler: submitHandler<FormValue> = (data) => { console.log(data) }
타입스크립트를 사용하기 때문에, 핸들러 타입은 submitHandler이고 <FormValue> 타입을 인자로 받는 제네릭 함수라고 분명하게 명시합니다.
handleSubmit 함수가 발동 하면, register를 등록해두었던 Form 필드 데이터 값이 인자에 전달됩니다. 그걸 data 라고 했을 때 이제 핸들러 함수 안에서 data 값을 자유자재로 다룰 수 있는 것입니다.
그래서 다시 저의 useMutation 코드로 돌아가본다면, title, problem, expected 세 가지 필드 데이터 값을 requestBody에 정리하여 POST 요청을 날려줍니다.
이제 register를 등록하여 데이터 값도 잘 받았고, POST 요청도 정상적으로 했으니 조금 더 디테일한 기능에 관심을 기울여볼 수 있겠습니다.
저는 react-hook-form 라이브러리를 공부하던 중 isSubmitting 을 활용한 중복 제출 막기 기능에 관심이 쏠렸습니다.
isSubmitting 이란, 양식이 현재 제출 상태인지 아닌지 파악하는 상태값 입니다. 초반에 useForm 훅을 통해 register, handleSubmit함수를 받아오면서 formState라는 걸 같이 가져왔었습니다. formState 속성은 현재 양식이 어떤 상태인지를 담고있는데 그 중 하나가 isSubmtting인 겁니다.
이를 사용하여 만약 제출 중이라면 버튼을 빠르게 연달아 클릭해도 Form이 중복 제출 되지 않도록 비활성화 할 수 있습니다.
저는 이렇게 button 요소에 disabled 속성을 활용하여 제출 중이라면 버튼이 비활성화 되도록 막았습니다.
<PrimaryBtn size="fit-content" className='mt-3 mb-14' disabled={isSubmitting}>Post your question</PrimaryBtn>
disabled 속성은 명시하지 않으면 자동으로 false 값이고, 명시하면 true 입니다. 저는 isSubmitting 값으로 지정해줬기 때문에 제출 중이면 true가 되어 비활성화 될 것이고, 제출 중이 아니면 false가 되어 버튼이 활성화 된 상태입니다.
이 외에도 useForm 훅을 통해 가져올 수 있는 다양한 함수와 활용할만한 상태값들이 많습니다. 특히 또 관심이 가는 건 watch 라는 것이었는데, register를 등록한 필드의 변경사항을 추적하는 함수입니다. 이를 활용하여 값을 추적한 다음 유효성 검증을 추가하면 더 디테일을 신경 쓴 결과물이 될 거 같습니다.
useMutation으로 POST 요청 보내기
이제 위에서 대충 넘어갔던 useMutation POST 요청에 대해 다시 정리해 보겠습니다.
이미 구현을 다 마치고 제출까지 한 다음 블로깅을 하는 거라 뒤늦게 '아 진짜 잘못 썼네' 라는 코드들 뿐이지만, 그럼에도 다음 Main-Project 때와 비교하기 위해 기록합니다. 학습용으로는 정말 불필요한 코드입니다😅
react-query 라이브러리에서 useMutation 훅을 불러옵니다.
import { useMutation } from '@tanstack/react-query';
useMutation 훅의 첫 번째 인자로 비동기 작업을 수행하는 함수를 등록합니다.
const mutation = useMutation(addNewQuestion);
훅은 mutation 객체를 반환하는데, 그 안에 다양한 함수와 상태값이 포함되어 있습니다. 객체 구조분해할당을 사용하면 각각 필요한 것만 뽑아 쓸 수 있습니다.
useMutation 훅에 전달한 비동기 함수입니다.
const addNewQuestion = (data:FieldValues) => { return call('/questions', 'POST', { memberId:isUser.memberId, title: data.title, content: data.problem + `\n\n` + data.expected, }); }
아직 msw 테스트를 위해 JWT이 아니라 memberId를 포함한 질문 관련 데이터를 담아 POST 요청을 보냅니다.
Submit 버튼을 누르면 아래 핸들러가 작동됩니다. mutation 객체에서 mutate를 호출하여 mutation을 실행시킵니다.
const onSubmit: SubmitHandler<FieldValues> = useCallback( (data:FieldValues) => { mutation.mutate(data, { onSettled:(data) => { window.scrollTo(0,0); navigate(`/questions/${data.questionId}`);; } }); }, [mutation] )
useMutation 훅에 첫번째 인자로 전달했던 비동기 함수를 실행시킵니다. data는 해당 함수에 전달하는 객체로, 지금은 FormValue가 됩니다.
onSettled는 mutation 실행 결과 상태 중 하나입니다. onSuccess, onSettled, onError가 있는데 그 중 onSettled는 mutation이 성공하든 실패하든 전달하는 결과를 기술합니다.
저 부분에서 부족한 점이 나타났습니다.
데이터가 정상적으로 POST 되었을 때 발생해야 하는 조건들이 onSettled에 들어가는 바람에 성공이든, 실패이든 상관 없이 실행됩니다. 처음에는 onSuccess 안에 작성했는데, POST 요청이 정상적으로 수행되어도 실행이 되지 않는 문제가 발생했습니다.
(작성중)
'Studying > Proj 과정' 카테고리의 다른 글
[Stack-Overflow 클론] 내일은 내일의 프로젝트가 있다 (0) 2023.06.27 [Stack-Overflow 클론] 질문 상세보기 페이지 기능 구현에서의 실수 (0) 2023.06.21 [Stack-Overflow 클론] 위젯, 로그인, 회원가입 UI 구현하기 (0) 2023.06.18 [Stack-Overflow 클론] Agile하게 프로젝트 진행하기 (0) 2023.06.13 [Stack-Overflow 클론] 요구사항 명세서 (0) 2023.06.13