- 기간
- 2025.09 – 2026.02
- 역할
- Frontend Engineer
- 팀
- (주)크루베이션
- 기술
- Next.js
- TypeScript
- Tailwind CSS
- React Query
- Recoil

사내 화면은 공개할 수 없어서 공개 페이지와 문제 해결 과정으로 대신합니다.
배경
대학과 기관이 학생들에게 제공하는 AI 취업 준비 서비스입니다. 실제 사용자가 계속 쓰는 서비스라서 새 기능만큼 기존 화면의 안정성과 반응 속도가 중요했습니다.
맡은 일
- 경험 정리, 자기소개서, 면접 준비 화면 개발과 운영
- 제휴 기관 관리자용 통계 화면 개편: 기관 전체와 학과별, 기능별 사용 현황과 화면별 엑셀 추출
- AI 답변 스트리밍, 모바일 대응, 공통 UI 문제 해결
- 기획, 디자인, 백엔드와 요구사항 조율
핵심 결정
결정 1
전체를 다시 불러오지 않고, 바뀐 한 건만 동기화했습니다
- 상황
- 경험 목록에서 상세 모달을 열고 닫기만 해도 목록 전체 API가 다시 호출되고 모든 카드가 다시 그려졌습니다.
- 선택
- 원인을 목록 조회 effect의 의존성, 배열 전체 교체, 즐겨찾기 조회 범위, 부모와 자식 렌더 결합의 네 가지로 나눴습니다. 모달을 닫을 때 수정된 한 건만 받아 병합하고, 즐겨찾기 조회 기준을 id 목록으로 좁히고, 카드를 memo로 분리한 뒤 콜백 참조를 고정했습니다.
- 이유와 결과
- 바뀌지 않은 항목의 객체 참조가 유지돼야 React가 렌더를 건너뛸 수 있습니다. 모달 여닫기로 인한 목록 재조회가 사라졌고, React Profiler 단일 측정 기준 렌더 시간도 15.7ms에서 13.5ms로 줄었습니다.
결정 2
채팅과 초안 생성에 서로 다른 방식을 썼습니다
- 상황
- AI 답변은 생성되는 대로 보여 주는 편이 좋지만, AI를 두 번 호출하는 자기소개서 초안 생성은 중간에 실패하면 추적과 재시도가 필요했습니다.
- 선택
- 채팅은 SSE(EventSource)로 받아 말풍선이 자라듯 표시하고, Next.js Route Handler를 프록시로 두어 CORS와 버퍼링 문제를 줄였습니다. 초안 생성은 추적 가능한 요청 방식을 유지하고 진행 표시로 대기 경험을 보완했습니다.
- 이유와 결과
- 초안 생성은 실패하면 어디서 멈췄는지 알고 다시 시도할 수 있어야 해서, 빨라 보이는 쪽 대신 추적이 되는 쪽을 골랐습니다.
결정 3
모바일에서는 hover 대신 탭으로 안내했습니다
- 상황
- 선택할 수 없는 항목의 안내 툴팁이 모바일에서만 깜빡이거나 다른 문구로 바뀌었습니다. 터치 환경에서 섞여 들어온 마우스 이벤트가 하나의 상태를 번갈아 덮어쓰고 있었습니다.
- 선택
- matchMedia로 hover 가능한 기기를 판별해 데스크톱은 hover, 모바일은 탭하면 1.5초 뒤 자동으로 닫히도록 입력 방식을 분리하고, 타이머 정리까지 커스텀 훅으로 옮겼습니다.
- 이유와 결과
- 입력 방식이 다른 기기에서 같은 이벤트 코드를 공유하면 상태가 경쟁합니다. 규칙을 훅 하나에 모아 다른 선택 화면에서도 같은 문제가 생기지 않게 했습니다.
코드로 보기
const handleClose = useCallback(async () => {
const closingId = openedItem?.id;
setOpenedItem(null);
if (closingId == null) return;
const detail = await fetchItemDetail(closingId);
// 목록 전체를 다시 받지 않고, 바뀐 한 건만 교체한다
setRows((prev) =>
prev.map((row) => (row.id === closingId ? mergeDetail(row, detail) : row)),
);
}, [openedItem]);바뀌지 않은 행은 기존 객체를 그대로 두기 때문에, memo로 분리한 카드는 다시 그려지지 않습니다.
결과
- 모달을 여닫는 것만으로 목록 전체를 다시 불러오던 요청이 사라지고, 수정 결과는 해당 항목에만 반영됩니다.
- 문제 해결 과정을 증상, 원인, 해결, 효과, 검증 순서로 남겨 같은 유형의 문제를 다시 찾아볼 수 있게 했습니다.