You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
isLoading, isError 상태를 각 axios 요청 함수별로 따로 리팩토링이 요구되는데, 다수의 반복작업 예상
Caching을 활용한 불필요한 요청 최소화 필요
React-query의 hook들은 이러한 불필요한 작업을 최소화하고, 필요시 hook에 적용될 수 있는 여러 메서드를 제공하기 때문에 추후 추가적인 요청사항이 있을 시 최소한의 코드로 요청사항 반영 가능
// 개선 전 코드// TODO 최초 페이지 진입 시 유저의 정보를 조회하는 코드import{useState,useEffect}from"react";useEffect(()=>{if(!isLoggedIn){navigate("/login");}constfetchMemberInfo=async()=>{try{constinfo=awaitgetMemberInfo(isLoggedIn);setMemberInfo(info);setIntroduceInfo({aboutMe: info.aboutMe,withMe: info.withMe});}catch(error){}};fetchMemberInfo();},[isModalOpen,isRendering]);
// 개선 후 코드import{useQuery}from"@tanstack/react-query";const{ data, isLoading, isError }=useQuery(["userInfo"],()=>{returngetMemberInfo(isLoggedIn);});constuserInfo=data;if(!isLoggedIn)navigate("/login");if(isLoading)return<div>로딩중...</div>;if(isError)return<div>에러가발생했습니다.</div>;
useMutation Hook 적용
isLoading, isError 상태를 각 axios 요청 함수별로 따로 리팩토링이 요구되는데, 다수의 반복작업 예상
수정/삭제 요청 쿼리의 단순화
// 개선 전 코드// TODO 최초 페이지 진입 시 유저의 정보를 조회하는 코드import{useState,useEffect}from"react";useEffect(()=>{if(!isLoggedIn){navigate("/login");}constfetchMemberInfo=async()=>{try{constinfo=awaitgetMemberInfo(isLoggedIn);setMemberInfo(info);setIntroduceInfo({aboutMe: info.aboutMe,withMe: info.withMe});}catch(error){}};fetchMemberInfo();},[isModalOpen,isRendering]);
// 개선 후 코드import{useQuery}from"@tanstack/react-query";const{ data, isLoading, isError }=useQuery(["userInfo"],()=>{returngetMemberInfo(isLoggedIn);});constuserInfo=data;if(!isLoggedIn)navigate("/login");if(isLoading)return<div>로딩중...</div>;if(isError)return<div>에러가발생했습니다.</div>;
Vitest를 활용한 React-testing
해시/리스트와 같은 자료구조를 활용한 함수 유닛 테스팅
describe("요일 맵핑 테스트",()=>{it("[0,0,0,0,0,0,1]이 파라미터로 전달되면 ['일']을 리턴한다",()=>{expect(DayOfWeekBinaryToStringMap([0,0,0,0,0,0,1])).toEqual(["일"]);});it("[1,0,0,1,1,0,0]이 파라미터로 전달되면 ['월', '일']을 리턴한다",()=>{expect(DayOfWeekBinaryToStringMap([1,0,0,1,1,0,0])).toEqual(["월","목","금",]);});it("[0,0,0,0,0,0,1]이 파라미터로 전달되면 ['0']을 리턴한다",()=>{expect(DayOfWeekBinaryToNumber([0,0,0,0,0,0,1])).toEqual(["0"]);});it("[0,1,0,0,1,0,0]이 파라미터로 전달되면 ['2', '5']을 리턴한다",()=>{expect(DayOfWeekBinaryToNumber([0,1,0,0,1,0,0])).toEqual(["2","5"]);});});describe("페이지네이션 테스트",()=>{it("totalPages의 값이 3일 경우 [1,2,3]이 출력된다.",()=>{expect(getPageArray(3)).toEqual([1,2,3]);});});describe("인코드 테스트",()=>{it("1이 입력될 경우 'MQ=='가 출력된다.",()=>{expect(encodedUrl(1)).toEqual("MQ==");});it("'nickName'이 입력될 경우 'bmlja05hbWU='가 출력된다",()=>{expect(encodedUrl("nickName")).toEqual("bmlja05hbWU=");});it("'test01'이 입력될 경우, 'dGVzdDAx'가 출력된다",()=>{expect(encodedUrl("test01")).toEqual("dGVzdDAx");});it("불리언 true 값이 입력될 경우 'dHJ1ZQ=='가 출력된다",()=>{expect(encodedUrl(true)).toEqual("dHJ1ZQ==");});});
isLoading, isError 상태를 각 axios 요청 함수별로 따로 리팩토링이 요구되는데, 다수의 반복작업 예상
수정/삭제 요청 쿼리의 단순화
// 개선 전 코드// TODO 최초 페이지 진입 시 유저의 정보를 조회하는 코드import{useState,useEffect}from"react";useEffect(()=>{if(!isLoggedIn){navigate("/login");}constfetchMemberInfo=async()=>{try{constinfo=awaitgetMemberInfo(isLoggedIn);setMemberInfo(info);setIntroduceInfo({aboutMe: info.aboutMe,withMe: info.withMe});}catch(error){}};fetchMemberInfo();},[isModalOpen,isRendering]);
// 개선 후 코드import{useQuery}from"@tanstack/react-query";const{ data, isLoading, isError }=useQuery(["userInfo"],()=>{returngetMemberInfo(isLoggedIn);})constuserInfo=data;if(!isLoggedIn)navigate("/login");if(isLoading)return<div>로딩중...</div>if(isError)return<div>에러가발생했습니다.</div>
Vitest를 활용한 React-testing
해시/리스트와 같은 자료구조를 활용한 함수 유닛 테스팅
describe("요일 맵핑 테스트",()=>{it("[0,0,0,0,0,0,1]이 파라미터로 전달되면 ['일']을 리턴한다",()=>{expect(DayOfWeekBinaryToStringMap([0,0,0,0,0,0,1])).toEqual(["일"]);});it("[1,0,0,1,1,0,0]이 파라미터로 전달되면 ['월', '일']을 리턴한다",()=>{expect(DayOfWeekBinaryToStringMap([1,0,0,1,1,0,0])).toEqual(["월","목","금"]);});it("[0,0,0,0,0,0,1]이 파라미터로 전달되면 ['0']을 리턴한다",()=>{expect(DayOfWeekBinaryToNumber([0,0,0,0,0,0,1])).toEqual(["0"]);});it("[0,1,0,0,1,0,0]이 파라미터로 전달되면 ['2', '5']을 리턴한다",()=>{expect(DayOfWeekBinaryToNumber([0,1,0,0,1,0,0])).toEqual(["2","5"]);});});describe("페이지네이션 테스트",()=>{it("totalPages의 값이 3일 경우 [1,2,3]이 출력된다.",()=>{expect(getPageArray(3)).toEqual([1,2,3]);});});describe("인코드 테스트",()=>{it("1이 입력될 경우 'MQ=='가 출력된다.",()=>{expect(encodedUrl(1)).toEqual("MQ==");});it("'nickName'이 입력될 경우 'bmlja05hbWU='가 출력된다",()=>{expect(encodedUrl("nickName")).toEqual("bmlja05hbWU=");});it("'test01'이 입력될 경우, 'dGVzdDAx'가 출력된다",()=>{expect(encodedUrl("test01")).toEqual("dGVzdDAx");});it("불리언 true 값이 입력될 경우 'dHJ1ZQ=='가 출력된다",()=>{expect(encodedUrl(true)).toEqual("dHJ1ZQ==");});});
쿼리 전송단계에서 intercept시 url의 query를 조작하여 해커가 원하는 데이터를 임의로 탈취당할 우려
이로 인해 쿼리 요청 단계에서 utf-8 형식을 base64 형식으로 인코딩하여 쿼리 요청 적용
// 개선 전 코드exportasyncfunctiongetStudyGroupInfo(id: number,isLoggedIn: boolean){if(!isLoggedIn)thrownewError("로그인 상태를 확인하세요");constresponse=awaittokenRequestApi.get<StudyInfoDto>(`/studygroup/${id}`);conststudyInfo=response.data;
...
returnstudyInfo;}
// 개선 후 코드exportasyncfunctiongetStudyGroupInfo(id: number,isLoggedIn: boolean){if(!isLoggedIn)thrownewError("로그인 상태를 확인하세요");constencodeId=Base64.encode(id.toString());constresponse=awaittokenRequestApi.get<StudyInfoDto>(`/studygroup/${encodeId}`);conststudyInfo=response.data;
...
returnstudyInfo;}
이미지 업로드 시, JSON 형식에서 Form-data 형식으로 변경
이미지 업로드 시, 서버 부하로 인해 RDS에 직접 저장 대신 S3에 저장하는 로직 구현 (서버 구현 사항)
이 과정에서 서버 측에서 JSON 형식으로 된 데이터가 아닌 Form-data 형식으로 된 쿼리를 요청
// 변경 전 코드// 현재 interface는 별도로 분리하여 관리// export interface MemberProfileUpdateImageDto {// profileImage: string;// }exportconstupdateMemberProfileImage=async(data: MemberProfileUpdateImageDto)=>{awaittokenRequestApi.patch("/members/profile-image",data);};
// 변경 후 코드exportconstupdateMemberProfileImage=async(image: MemberProfileUpdateImageDto)=>{if(!image.image)thrownewError("이미지를 확인해주세요.");constformData=newFormData();formData.append("image",image.image);awaittokenRequestApi.patch("/members/image",formData,{headers: {"Content-Type": "multipart/form-data",},});};
인터페이스 모듈화 및 분리
// 개선 전 코드// ~/src/apis/MemberApi 에서 interface 타입 및 api 요청 통합 관리// TODO : 유저정보 get 요청 DTO 타입 정의exportinterfaceMemberInfoResponseDto{uuid: string;email: string;profileImage: string;nickName: string;aboutMe: string;withMe: string;memberStatus: "MEMBER_ACTIVE"|"MEMBER_INACTIVE";roles: string[];}// TODO: 유저정보 get 요청하는 axios 코드exportconstgetMemberInfo=async(isLoggedIn: boolean)=>{if(!isLoggedIn)thrownewError("로그인 상태를 확인해주세요.");// tokenRequestApi를 사용하여 /members 엔드포인트로 GET 요청 전송constresponse=awaittokenRequestApi.get<MemberInfoResponseDto>("/members");// 응답 데이터 추출constdata=response.data;returndata;// 데이터 반환};
// 개선 후 코드// types/MemberApiInterfacesimport{MemberInfoResponseDto}from"../types/MemberApiInterfaces";// /src/apis/MemberApiexportconstgetMemberInfo=async(isLoggedIn: boolean)=>{if(!isLoggedIn)thrownewError("로그인 상태를 확인해주세요.");constresponse=awaittokenRequestApi.get<MemberInfoResponseDto>("/members");constdata=response.data;returndata;};
// ~/src/pages/StudyList// ... dependancyimport{useInView}from"react-intersection-observer";constStudyList=()=>{const[ref,inView]=useInView();const[list,setList]=useState<StudyGroupListDto[]>([]);const[currentPage,setCurrentPage]=useState(1);// ... 기타 states (재정렬 및 데이터 편집)constnavigate=useNavigate();useEffect(()=>{if(inView)fetchList();},[inView]);constfetchList=async()=>{constres=awaitgetStudyGroupList(currentPage);setList((prev)=>[...prev, ...res]);setCurrentPage((prevPage)=>prevPage+1);};useEffect(()=>{filterList(sortValue);},[list]);consthandleSortOrder=(e: React.ChangeEvent<HTMLSelectElement>)=>{// ... select option에 따른 이벤트 핸들링};constfilterList=(sortValue: string)=>{// ... 재정렬 조건문};return(<StudyListContainer><StudyListBody>// ...</StudyListTop><ListFilterWrapper>// ...</ListFilterWrapper><StudyBoxContainer>{filterData?.map((item: StudyGroupListDto)=>(<StudyBoxkey={item?.id}onClick={()=>navigate(`/studycontent/${item?.id}`)}><StudyListImageimage={item.image}></StudyListImage><div><divclassName="studylist-title"><h3>{item?.title}</h3></div><divclassName="studylist-interest"><divid="studylist-interest_likes">❤️{item?.likes}</div><divid="studylist-interest_views">🧐{item?.views}</div></div><divclassName="studylist-tag"><StudyListTagitem={item.tags}/></div></div></StudyBox>))}</StudyBoxContainer></StudyListBody><divref={ref}></div></StudyListContainer>);};exportdefaultStudyList;