새로운 프로젝트를 진행하며 한 컴포넌트에서 많은 이미지를 사용해야하는 기능 구현을 하게되었다.
각 1번, 2번, 3번, 4번 선택지마다 적게는 5개의 이미지 파일, 많게는 7개의 이미지 파일을 사용자에게 보여주어야 한다.
이미지는 이미지 서버에서 GET 요청으로 가져오는 방식으로 구현하려 한다.
이번 프로젝트에서는 MSW를 이용해 테스트 환경을 구축해서 개발한다.
http.get("/champions/duelist", async () => {
return HttpResponse.json(
{
"champions": [
{
"id": 1,
"imageUrl": "https://대충 이미지 서버 주소/valorant/duelist/ZETT.webp"
},
{
"id": 2,
"imageUrl": "https://대충 이미지 서버 주소/valorant/duelist/REYNA.webp"
},
{
"id": 3,
"imageUrl": "https://대충 이미지 서버 주소/valorant/duelist/RAZE.webp"
},
{
"id": 4,
"imageUrl": "https://대충 이미지 서버 주소/valorant/duelist/PHOENIX.webp"
},
{
"id": 5,
"imageUrl": "https://대충 이미지 서버 주소/valorant/duelist/NEON.webp"
},
{
"id": 6,
"imageUrl": "https://대충 이미지 서버 주소/valorant/duelist/YORU.webp"
},
{
"id": 7,
"imageUrl": "https://대충 이미지 서버 주소/valorant/duelist/ISO.webp"
},
]
}
)
}),
http.get("/champions/initiator", async () => {
return HttpResponse.json(
{
"champions": [
{
"id": 1,
"imageUrl": "https://대충 이미지 서버 주소/valorant/initiator/BREACH.webp"
},
{
"id": 2,
"imageUrl": "https://대충 이미지 서버 주소/valorant/initiator/SOVA.webp"
},
{
"id": 3,
"imageUrl": "https://대충 이미지 서버 주소/valorant/initiator/SKYE.webp"
},
{
"id": 4,
"imageUrl": "https://대충 이미지 서버 주소/valorant/initiator/FADE.webp"
},
{
"id": 5,
"imageUrl": "https://대충 이미지 서버 주소/valorant/initiator/KAY_O.webp"
},
{
"id": 6,
"imageUrl": "https://대충 이미지 서버 주소/valorant/initiator/GEKKO.webp"
},
]
}
)
}),
http.get("/champions/sentinel", async () => {
return HttpResponse.json(
{
"champions": [
{
"id": 1,
"imageUrl": "https://대충 이미지 서버 주소/valorant/sentinel/SAGE.webp"
},
{
"id": 2,
"imageUrl": "https://대충 이미지 서버 주소/valorant/sentinel/CYPHER.webp"
},
{
"id": 3,
"imageUrl": "https://대충 이미지 서버 주소/valorant/sentinel/CHAMBER.webp"
},
{
"id": 4,
"imageUrl": "https://대충 이미지 서버 주소/valorant/sentinel/KILLJOY.webp"
},
{
"id": 5,
"imageUrl": "https://대충 이미지 서버 주소/valorant/sentinel/DEADLOCK.webp"
},
]
}
)
}),
http.get("/champions/controller", async () => {
return HttpResponse.json(
{
"champions": [
{
"id": 1,
"imageUrl": "https://대충 이미지 서버 주소/valorant/controller/OMEN.webp"
},
{
"id": 2,
"imageUrl": "https://대충 이미지 서버 주소/valorant/controller/VIPER.webp"
},
{
"id": 3,
"imageUrl": "https://대충 이미지 서버 주소/valorant/controller/BRIMSTONE.webp"
},
{
"id": 4,
"imageUrl": "https://대충 이미지 서버 주소/valorant/controller/ASTRA.webp"
},
{
"id": 5,
"imageUrl": "https://대충 이미지 서버 주소/valorant/controller/HARBOR.webp"
},
]
}
)
}),
mock 데이터를 만든뒤 해당 Url로 HTTP GET 요청을 보내면 각 기준으로 분류된 이미지들의 id와 이미지 주소를 받게된다.
문제
하지만 여기서 고민된다...
분류별로 나뉘어진 이미지를 한번에 가져올지 아니면 사진과 같이 각 상황에 맞게 이미지를 가져올지 고민된다.
이 고민은 모든 이미지를 한번에 가져오기로 했다.
이유는 두 가지였다.
첫 번째 이유는 유저가 각 이미지별 선택한 이미지의 정보를 저장해야하는데, 이때 각각 이미지를 불러오게되면 상태를 관리하는 로직이 너무 복잡해지기 때문이다. 가장 큰 이유였던 두 번째 이유는 매번 이미지 정보를 요청으로 인한 이미지 서버 부하와 렌더링 시간의 최적화 때문이다.
이러한 구조를 갖고있는데, 기존에 각 이미지 정보 데이터를 받아오면 로직이 너무 복잡해졌다.
이로인해 에러가 발생해도 직관적으로 문제를 해결하기 힘들었다.
또한 반복적으로 요청을 하게되면 혹시나 하는 이미지 서버 부하를 걱정했고, 이미지 정보를 받아올때 마다 데이터를 가공해 사용하기에는 유저가 빠르게 조작할 수 있는 기능이기에 렌더링 속도가 걱정되었다.
이러한 문제를 해결하기 위해 useQueries를 적용했다.
useQueries 이용해 병렬 호출
useQueries를 이용해 병렬로 API를 비동기적으로 호출하면 데이터를 한번에 받아 관리하기 쉬워진다.
const ShowPostion = ({ positions }: positionsInterface) => {
const ePositions = positions.map(e => e.ePosition);
const info = useQueries({
queries: ePositions.map(position => ({
queryKey: ['image', position],
queryFn: async () => {
const response = (await axios.get(`/champions/${position}`)).data;
return response.champions;
},
refetchOnWindowFocus: false, // 브라우저 창 포커스에 따른 자동 재요청 비활성화
cacheTime: Infinity, // 최초 호출 후 바뀌지 않는 정적 데이터라 cacheTime과 staleTime을 Infinity로 설정
staleTime: Infinity
}))
});
const isLoadings = info.some(e => e.isLoading === true);
// 객체형으로 선택됬는지 여부를 보관하기
const [selectedState, setSelectedState] = useState<selectedStateInterface>({
position: {
duelist: false,
recon: false,
sentinels: false,
controllers: false,
noDicision: false,
},
operator: []
});
// API에 정보 전달을 위해 모아두는 상태?
const [selected, setSelected] = useState<selectedInterface>({
position: [],
operator: [],
});
// 포지션별 이미지들
const [positionImg, setPositionImg] = useState<positonImgInterface>({
duelist: [],
recon: [],
sentinels: [],
controllers: [],
});
/**
* 포지션 선택시 선택한 포지션의 상태를 갱신해주는 함수
* (리팩토링 필수)
* @param ePosition
*/
const selectPosition = (ePosition: string) => {
const existIdx = selected.position.findIndex(e => e === ePosition);
// 선택한 포지션을 다시 눌렀을 때 제거해주는 로직
if (existIdx !== -1) {
const remainPos = selected.position.filter(e => e !== ePosition)[0];
setSelected({
position: [...selected.position].filter(e => e !== ePosition),
operator: [...selected.operator.filter(e => e[0] !== ePosition)],
});
setSelectedState({
position: { ...selectedState.position, [ePosition]: false },
operator: remainPos ? positionImg[remainPos] : []
});
}
// 포지션 추가 로직
else if (selected.position.length < 2 && existIdx === -1) {
setSelected({
...selected,
position: [...selected.position, ePosition]
});
setSelectedState({
position: { ...selectedState.position, [ePosition]: true },
operator: [...positionImg[ePosition]]
});
}
else {
alert('포지션은 2개 까지 선택 가능합니다.(수정 예정)');
}
};
/**
* 선택한 포지션의 오퍼레이터를 선택하는 함수이며 2개까지 선택 가능하다.
* @param ePosition
* @param opName
* @param idx
*/
const selectOperator = (ePosition: string, opName: string, idx: number): void => {
const existCheck = selected.operator.filter(e => e[1] === opName);
const clickedOpPos = [...positionImg[ePosition]];
// 선택한 포지션을 다시 눌렀을 때 제거해주는 로직
if (existCheck.length !== 0) {
clickedOpPos[idx].state = false;
setSelected({
...selected,
operator: [...selected.operator].filter(e => e[1] !== opName)
});
setPositionImg({
...positionImg,
[ePosition]: clickedOpPos
});
}
else if (selected.operator.length < 2 && existCheck.length === 0) {
clickedOpPos[idx].state = true;
setSelected({
...selected,
operator: [...selected.operator, [ePosition, opName]]
});
setPositionImg({
...positionImg,
[ePosition]: clickedOpPos
});
}
else {
alert('주 요원은 2개 까지 선택 가능합니다.(수정 예정)');
}
}
// 페이지 로드와 동시에 useQueries의 loading이 끝나면 이미지 정보들을 저장하는 부분
useEffect(() => {
const test: positonImgInterface = {};
if (info && !isLoadings) {
info.map((posInfo, idx) => {
const position = ePositions[idx]
const posImg = posInfo.data.map((opImg: ImgInterface) => ({
id: opImg.id,
imageUrl: opImg.imageUrl,
state: false,
name: checkPosAndOp(opImg.imageUrl)[1],
position: position,
}));
test[position] = posImg;
});
setPositionImg(test);
}
}, [isLoadings]);
return (
...
)
};
결과
위의 코드로 다시 작성하면서 얻은 효과는 다음과 같다.
불러온 이미지가 정적으로 있기 때문에 실시간으로 선택한 포지션별 이미지가 변해서 선택한 이미지를 상태를 관리하는 것보다 상태 관리가 쉬워졌다.
또한, 이미지를 한번에 받아와 가공해 사용하기에 초기 렌더링에 투자를 한다면 유저의 조작에 따른 렌더링 속도에 주는 영향은 줄일 수 있었다.
이번 작업은 실시간으로 받아오던 이미지 데이터를 초기 렌더링시 한번에 불러와 정적인 상태에서 관리할 수 있게해주는 작업이였다.
이 작업을 통해 useQuery와 useQueries를 사용할 때 cacheTime과 staleTime에 대해 알게되었다.
추후에는 TanstackQuery의 useQuery와 useQueries의 cacheTime과 staleTime에 대해 정리해보겠다.
useQuery를 이용하고 cacheTIme과 staleTime을 설정해주면 이런 작업은 필요없지 않았나 라는 고민을 했지만, 받아온 데이터를 한번 가공을 해서 상태관리를 해주기 때문에 결국 데이터를 받아올 때마다 가공을 해야하기에 동작 속도에 영향을 줄거라 생각했다.
참고한 글
'FE 이모저모 공부' 카테고리의 다른 글
에자일 (Agile) 개발론 (0) | 2024.04.23 |
---|---|
에러) jest syntaxerror: cannot use import statement outside a module (0) | 2024.03.05 |
React-Query에 대하여 (0) | 2024.01.18 |
Testing-library를 이용해 React 컴포넌트 테스팅 (0) | 2024.01.12 |
Emotion 가볍게 공부하기 (0) | 2024.01.05 |