Plers · 2026.02 – 2026.07
Frontend · FE 2 / BE 1 / 기획 / 디자인
AI 에이전트 대화 기반 채용 스크리닝 B2B SaaS
ReactTypeScriptTanStack QueryZustandTailwindEmotionFSD
구현 기능
- 채팅 리스트 리렌더링 최적화 (커밋 40.3ms → 27.1ms)
- 연속 검색 경쟁 상태(race condition) 수정
- 멀티턴 대화형 검색 설계 (/search → /search/refine)
- AI 에이전트 대화 히스토리 · 입력 UI
- 후보자 프로필 카드 및 트렌딩 지원자 위젯 UI
- 휴대폰 인증 기반 비밀번호 재설정 플로우
트러블슈팅
1. 채팅 리스트 리렌더링 최적화
- 문제
- 후보자 카드 클릭으로 선택 상태만 바뀌어도 전체 메시지 리스트가 다시 렌더링됐습니다.
- 원인
- 메시지 렌더링 JSX가 map 콜백 안에 그대로 인라인되어 있어 선택 상태가 바뀔 때마다 화면의 모든 메시지 블록이 함께 리렌더됐습니다.
- 해결
- 응답 블록을 AgentMessageBlock 컴포넌트로 분리해 React.memo로 감쌌습니다.
- 선택 핸들러를 useCallback으로 안정화해 memo가 실제로 동작하도록 했습니다.
- 다른 카드를 클릭해도 나머지 메시지가 재렌더되지 않는지 검증하는 회귀 테스트를 추가했습니다.
src/widgets/agent-chatting/ui/AgentMessageBlock.tsx
export const AgentMessageBlock = memo(AgentMessageBlockBase);src/widgets/agent-chatting/ui/AgentChattingView.tsx
const handleSelectCandidate = useCallback((id: string) => {
setSelectedCandidates((prev) => toggle(prev, id));
}, []);AgentChattingView.test.tsx
// 카드 클릭 시 다른 메시지 블록은 재렌더되지 않아야 한다
expect(added).toBeLessThan(4);결과: 리렌더 커밋 40.3ms → 27.1ms, 재렌더되는 카드 5~6개 → 1개
2. 연속 검색 경쟁 상태 수정
- 문제
- 빠르게 연속으로 검색할 때 먼저 보낸 느린 요청의 응답이 나중에 도착해 최신 결과를 덮어쓰는 문제가 있었습니다.
- 원인
- 이전 요청을 취소하거나 응답 순서를 검증하지 않고, 도착하는 대로 상태에 그대로 반영하고 있었습니다.
- 해결
- 레이스 컨디션을 재현하는 회귀 테스트로 먼저 검증했습니다.
- 요청마다 AbortController로 이전 요청을 취소하고, 요청 시퀀스 번호를 비교해 최신 응답만 반영하도록 가드를 추가했습니다.
src/entities/agent-chat/model/useChatStore.ts
inFlightController?.abort();
inFlightController = new AbortController();
const seq = ++requestSeq;
const result = await search(query, { signal: inFlightController.signal });
if (seq !== requestSeq) return; // 더 최신 요청이 이미 진행 중이면 무시
applyResult(result);결과: 연속 검색 시 늦게 도착한 이전 응답이 최신 결과를 덮어쓰는 문제 해결