개요
api 호출(Postman) + api 테스트(Jmeter) 할 수 있는 웹 앱
- 배포 주소: https://api-test-project.vercel.app/
- 기간: 2023.3 ~ 2024.2
- 1년 내내 집중해서 한 것은 아니고 실제 개발 기간은 퇴근 후, 주말에 조금씩 해서 1~2달 정도 소요
- 인원: 프론트엔드 1명(나), 백엔드 1명
- 내 역할: 프론트엔드 전체
- 내가 사용한 기술: React 18, Framer-motion, emotion, Recoil, React Query, Chart.js
개발
실시간 대용량 데이터 처리 최적화
API 테스트(Jmeter)의 경우
- 자체 서버와 websocket으로 연결한 후에
- 최초 메시지로 테스트할 정보(사용자(쓰레드) 수, 호출 횟수 등)을 보내고
- 서버에서 해당 api를 주기적으로 호출한 후에 결과를 받자마자 그 결과를 websocket을 통해 클라이언트로 보내주는 형태로 구성되어 있다.
처음에는 웹소켓으로 메시지가 올 때마다 상태를 업데이트 해주는 식으로 개발을 했다.
사용자(쓰레드) 수, 호출횟수가 적을 때는 문제가 없었지만 많아질수록 브라우저에서 엄청난 렉이 걸리고 일정 횟수 이상일 때는 아예 브라우저가 죽었다.
문제를 찾아보니 크게 2가지였다.
- 너무 잦은 상태 업데이트
- 브라우저는 1초에 60프레임을 그려야 부드럽게 화면이 보여진다. 상태 업데이트를 너무 자주하다보니 60프레임 이상을 그리게 되서 버벅임(Jank) 발생
- 너무 많은 dom
- '결과 트리' 탭에서 모든 api 호출 결과들을 목록, 상세 형태로 조회할 수 있다.
- api 호출 결과들이 몇천, 몇만 건이 되니 너무 많은 dom이 만들어져서 버벅임이 발생
2가지 문제를 해결한 방법은 다음과 같다.
상태 업데이트 일정 주기(500ms)마다 하도록 변경
- 웹소켓에서 메시지가 올 때마다 상태를 업데이트 해주는 대신, 큐에 담도록 했다.
- 그리고 해당 큐에서 500ms마다 데이터를 꺼내서 상태를 업데이트하도록 변경했다.
- 변경 후에 렉이 많이 줄어들었다
- 해당 로직은 커스텀 훅으로 따로 분리해서 재사용할 수 있도록 했다.
let GLOBALQUEUE = []
export const useQueuing = ({ websocketUrl, onOpen, startMsg, onQueue, onClose }) => {
/// 생략..
const startTick = useCallback(() => {
const timer = setInterval(() => {
if (GLOBALQUEUE.length !== 0) {
const t = [...GLOBALQUEUE]
GLOBALQUEUE = []
if (onQueue) {
onQueue?.(t)
} else {
setQueue(prev => {
return [...prev, ...t]
})
}
}
}, 500)
return timer
}, [onQueue])
useEffect(() => {
if (lastMessage != null) {
if (isRealtime(lastMessage)) {
GLOBALQUEUE.push(lastMessage)
} else if (isOpen(lastMessage)) {
if (startMsg != null) {
sendMessage(startMsg)
}
onOpen?.(lastMessage)
timer = startTick()
} else if (isClose(lastMessage)) {
onClose?.(lastMessage)
setTimeout(() => {
clearInterval(timer)
}, 2000)
}
}
}, [lastMessage, onClose, onOpen, sendMessage, startMsg, startTick])
return [queue]
}
- windowing을 통해 목록에 보여지는 dom만 렌더링하도록 변경
- 'react-window'를 사용해 windowing을 적용했다.
- 수천, 수만건의 항목이 있는 목록에서 보여지는 부분만 렌더링하도록 변경하니, 해당 목록이 있는 '결과 트리' 탭의 버벅임이 사라졌다.
- 'react-window'는 쓰는 방법도 매우 간단하다. 좋은 라이브러리다. 번들 크기는 아직 체크를 안해봤다.
애니메이션을 컴포넌트로 분리
웹에서 과하지 않은 적절한 애니메이션은 사용자 경험을 향상시킬 수 있다.
- 변경되는 부분을 자연스럽게 강조
- 사용자의 다음 행동을 어느정도 유도할 수도 있다. (ex: 특정 항목 클릭시 변경되는 부분 반짝임(opacity;0 -> opacity: 1)해서 해당 부분에 집중하도록 유도)
- 그러나, 너무 과한 애니메이션은 사용자에 불편함을 줄 수 있다. 정신 없다.
그리고 개인적으로 웹이 세련되어 보이는 것 같다.
이번 프로젝트에는 애니메이션 주는 부분을 잘 추상화한 라이브러리 'framer-motion'을 활용했다.
'framer-motion'은 애니메이션 주는 것을 animate={{opacity: 1}} 이렇게 prop으로 줘서 직관성 있고 exit 애니메이션도 쉽게 구현 가능하다.
'framer-motion'을 쓰다보면 중복해서 쓰는 애니메이션이 많다. 그 중 하나인 반짝임 효과(opacity;0 -> opacity: 1)를 따로 컴포넌트 Blinker로 분리해서 사용했다.
프로젝트 내내 매우 요긴하게 썼다. step 상태가 변경될 때 렌더링 되는 내용을 변경하는 컴포넌트인 Funnel과 조합해서 많이 썼다.
<Blinker _key={step.code}>
<Funnel step={step.code}>
<div css={flexCss}>
<Funnel.Step name="ResultTree">
<ResultTree APITestResponses={APITestResponses} />
</Funnel.Step>
<Funnel.Step name="SummaryReport">
<SummaryReport summaryReport={summaryReport} />
</Funnel.Step>
<Funnel.Step name="ResultGraph">
<ResultGraph graphData={graphData} />
</Funnel.Step>
</div>
</Funnel>
</Blinker>
Blinker 컴포넌트는 내부 로직은 매우 간단하다. key가 변경되면 반짝인다.
export interface BlinkerProps {
_key: string
children: ReactNode
}
export const Blinker: React.FC<BlinkerProps> = ({ _key, children }) => {
return (
<AnimatePresence mode="wait">
<motion.div
key={_key}
css={blinkerCss}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.25 }}
>
{children}
</motion.div>
</AnimatePresence>
)
}
AnimatePresence
밑의 motion.div
의 key가 변경되면 리액트는 해당 컴포넌트 트리를 새걸로 인식한다. 그래서 이전 컴포넌트가 언마운트되고 그 후 에 새 것이 마운트된다. 그래서motion.div
의 키를 바꿔줘서 반짝임 효과를 줄 수 있다.
잘한 점
- 실시간 대용량 데이터 처리 최적화
- windowing
- 웹소켓 메시지 올 때마다 queue에 넣고 특정시간마다 처리하기
- 적절한 애니메이션 (ex: 페이지 변경될 때)
- framer-motion 활용
- 컴포넌트화 - Blinker(key 바뀔때마다 깜빡임)
아쉬운 점
- api payload 편집기의 각 input을 제어로 놔서 재렌더링이 너무 많다.
- 비제어로 놓고, 동적으로 폼 관리하기 편하게 'react-hook-form'을 활용하면 좋을 듯
- 성능 테스트 그래프
- react-chartjs의 직선 그래프 기본 형태를 사용함
- 직관적이지 못함
- 데이터 많이 늘어나면 엄청 버벅임
- 그래프 디자인 가이드, 대시보드 가이드 등을 찾아보면서 업그레이드 할 것
- 아키텍처
- api 테스트인데 서버를 따로 두어서 서버를 통해서 테스트하고자 하는 api를 찌르고 그 결과를 받아오는 구조
- 때문에, 서버의 성능만큼만 테스트를 할 수 있다. 서버 없이 구성하는 게 맞았을 듯
- 번들 최적화 안함
- 번들이 4mb인가 크기가 엄청 크다.
- 코드 스플릿 활용해서 줄여야 한다.