- Published on
Fitin
- Authors

- Name
- 이지영
| AI 기반 기업 컬쳐핏 분석 플랫폼
🗓️ 기간: 2025.06.30 ~ 2025.07.29
👥 팀 구성: UI/UX 디자이너 1명 + 프론트엔드 1명 + 백엔드 3명
👩💻 담당 역할: 팀장 & 프론트엔드 전담 개발
☑️ 전체 일정 조율 및 PM과의 커뮤니케이션
☑️ 노션 기반 문서 및 기획안 관리
☑️ 프론트엔드 주요 기능 개발 및 배포
🛠️ 사용 기술
📍 프론트엔드: HTML5, CSS3, TailwindCSS, JavaScript, Chart.js, Typescript, React.js, Zustand, Tanstack Query 📍 백엔드: Java, Spring security, OAuth2, JWT, MySQL, Swagger, Spring Boot
📍 AI 분석: Python Fast API, scikit-learn
🌿 깃허브
🖥️ 배포 도구 move to site
📍 프론트엔드: Vercel
📍 백엔드: Render(FastAPI), AWS(Spring Boot + MySQL)
❗NOTICE️
🚧 현재 배포 서버(api.heun0.site)에서 502 Bad Gateway 오류가 발생하고 있습니다.
- 확인 결과, 서버의 도메인 만료 및 DNS 연결 문제로 인해 API가 정상적으로 응답하지 않는 상태입니다.
- 프로젝트 기능은 정상 구현되어 있으며, 시연 영상을 통해 전체 흐름 및 기능 구동 모습을 확인하실 수 있습니다.
📄 참고 문서
구현 방식 : React 기반 CSR 방식의 설문조사 플랫폼
프로젝트 개요 : IT 교육기관 ‘멋쟁이사자처럼’ 주관의 단기 인턴십 프로젝트로, 인사담당자의 설문 응답 데이터를 기반으로 군집화 알고리즘을 적용해 채용 성향을 분석한 뒤 그 결과를 차트 형태로 시각화하여 제공하는 웹 애플리케이션입니다. 이를 통해 인사담당자는 채용 과정에서 참고할 수 있는 기준과 인사이트를 얻고, ‘멋쟁이사자처럼’의 수강생과의 조직문화 적합도(Culture Fit)를 분석할 수 있습니다.
프로젝트 진행과정 : 멋쟁이사자처럼에서 제공한 한 달간의 프로젝트 타임라인을 기반으로, 초반 2주간은 MVP 개발 목표에 집중하여 핵심 기능을 빠르게 구현했습니다.
이 기간 동안 UI 마크업과 설문 테스트 기능을 완성하고, Chart.js를 활용한 결과 시각화 페이지까지 구축하였습니다. 이후에는 기능 고도화, 버그 수정, 코드 리팩토링을 거치며 서비스의 완성도와 안정성을 높였습니다.
매일 30분 스탠드업을 통해 각 팀원들의 작업 현황을 점검하고, 타임라인에 맞춰 당일 반드시 완료해야 할 기능의 우선순위를 설정했습니다. PM과의 커뮤니케이션을 통해 일정 리스크를 사전에 관리하며 개발이 데드라인 내에 진행되도록 조율했습니다.
팀 전원이 AI 지식이 부족한 상황이었기에 k-means 핵심 개념을 직접 정리하고 회의록·노션 문서로 체계화하여 학습 기반을 마련했습니다. 또한 데일리 스크럼 후 회의 내용을 문서화하고, MVP 시연을 주도하며 팀 내 AI 스터디를 독려했습니다.
인턴십 시간이 하루 3시간으로 제한적이었던 만큼, 근무 시간 외에도 디스코드를 통해 실시간으로 소통하며 작업 흐름이 끊기지 않도록 했습니다. Figma를 활용해 UI/UX 디자이너와의 협업 과정(피드백 및 수정 사항)을 꼼꼼하게 기록하고, 백엔드 개발자들과 기능 구현 시 발생하는 불확실한 사항을 실시간으로 공유하고 피드백을 주고받으며, 당일 내 기능이 완료될 수 있도록 항상 적극적인 자세로 임했습니다.
그 결과, 핵심 데모 시나리오를 모두 충족하며 데드라인 내 배포를 완료했고, 실무에 가까운 협업·문서화·일정 관리 역량을 함께 강화할 수 있었습니다.
🚀 핵심 기능 구현
- 사용자 인증의 편의성 확보를 위해 쿠키 기반 인증 방식을 적용한 OAuth 2.0 로그인 및 세션 유지 로직 이해 및 구현
- AI 분석 응답 대기 과정에서의 UX 개선을 위해 polling 기반의 useRef 메모리 관리 방식으로 성능 최적화
- 30초 이내 AI 분석 응답 미도착 시 에러 페이지를 반환하는 예외 처리로 UX 안정성 향상
- 분석 결과의 가독성과 직관성 향상을 위해 Chart.js를 활용하여 사용자의 성향 데이터를 시각화
- 비인증 사용자 접근을 제한하는 목적의 인증 여부 판별 Protected Routes 컴포넌트 구현 및 fromSession 상태값 기반 접근 제어 로직 설계로 보안성 강화
- 결과 이미지를 다운로드할 수 있는 사용자 인터랙션 기능 구현하여 분석 결과의 외부 활용성 확대
공통 레이아웃 컴포넌트 설계로 페이지별 중복 제거 및 스타일 일관성 확보
피그마 시안을 분석하던 중, 모든 페이지가 동일한 레이아웃 구조를 공유하고 있다는 점을 발견했습니다. 이를 보고 “굳이 매번 같은 Header와 Footer를 반복 구현할 필요가 없겠다”고 판단했고, 하나의 공통된 틀 안에서 스타일링과 요소의 추가 여부를 커스터마이징할 수 있도록 설계했습니다.
구조적 일관성을 위한 컴포지션(Composition) 패턴 적용
페이지마다 구조는 조금씩 달라도 상단(Header)과 하단(Footer)은 일관된 형태를 유지할 수 있도록 공통 구조는 고정하되, 내부 콘텐츠는 props로 자유롭게 주입할 수 있도록 컴포지션 패턴 기반의 레이아웃을 설계하였습니다.
즉, Header/Footer 영역에 들어갈 콘텐츠를 ReactNode 기반의 named slot props(leftSlot, middleSlot, rightSlot, primaryBtn, secondaryBtn)로 주입하여, 전체적인 레이아웃 구조는 유지하면서도 내부 구조는 props를 이용해 페이지별로 유연한 커스터마이징이 가능하도록 설계했습니다.
Tailwind 기반 스타일 통합 관리
또한, TailwindCSS의 유틸리티 클래스를 중앙에서 통합 관리하여 스타일 중복을 최소화하고, 유지보수성을 향상시켰습니다. 레이아웃 컴포넌트의 containerCN, mainCN, footerCN과 같은 prop을 통해 페이지 특성에 따라 배경색, 레이아웃 간격, 정렬 방식 등을 손쉽게 커스터마이징할 수 있게 구현했습니다.
이렇게 만든 Tailwind + TypeScript 기반의 SurveyLayout 컴포넌트를 모든 페이지(pages/*)의 최상위 레이아웃으로 재사용함으로써 코드 중복을 줄이고 스타일링 시간을 크게 단축시킬 수 있었습니다.
import Footer from "@/components/layouts/Footer";
import Header from "@components/layouts/Header";
import { SurveyLayoutProps } from "@/models/survey";
export default function SurveyLayout({
children,
leftSlot,
middleSlot,
rightSlot,
primaryBtn,
secondaryBtn,
containerCN = "bg-white",
mainCN = "",
footerCN,
}: SurveyLayoutProps) {
return (
<div
className={`max-w-[640px] mx-auto flex flex-col min-h-screen ${containerCN}`}
>
{/* 상단 네비게이션 or 진행 표시 */}
<Header
leftSlot={leftSlot}
middleSlot={middleSlot}
rightSlot={rightSlot}
/>
{/* 콘텐츠 영역 */}
{/* Tailwind에서는 items-* 클래스는 서로 덮어쓰기 되지 않고 공존 => mainClassName으로 스타일링 조정 */}
<main className={`flex flex-col flex-1 overflow-y-auto ${mainCN}`}>
{children}
</main>
{/* 하단 버튼 영역 */}
<Footer
primaryBtn={primaryBtn}
secondaryBtn={secondaryBtn}
footerCN={footerCN}
/>
</div>
);
}
ex. SurveyLayout 적용: 최근 결과 상세 페이지 구조
export default function HistoryPage() {
const navigate = useNavigate()
const axios = useAxiosInstance()
const { user } = useUserStore()
const { resultId } = useParams()
const { captureDiv } = useCapture()
const divRef = useRef < HTMLDivElement > null
const handleCapture = () => {
if (!user || !imageName) return
captureDiv(divRef, `${user.nickname}_${imageName}.png`)
}
const {
data: detail,
isLoading,
isError,
error,
} = useQuery({
queryKey: ['result'],
queryFn: async () => {
const res = await axios.get(`/api/v1/result/analysis/${resultId}`)
return res.data
},
})
const imageName = convertToSvgPath(detail?.analysisResponseDto.imageUrl)
return (
<SurveyLayout
containerCN="bg-grey-20"
leftSlot={
<button onClick={() => navigate(-1)}>
<img src={`/chevron-left.svg`} className="aspect-square w-[2.4rem] lg:w-[3.2rem]" />
</button>
}
middleSlot={<img src={`/logo-s.svg`} className="w-[5rem] lg:w-[6rem]" />}
rightSlot={
<button>
<img
src={`/download.svg`}
className="aspect-square w-[2.4rem] lg:w-[3.2rem]"
onClick={() => handleCapture()}
/>
</button>
}
mainCN="px-[2rem] pt-[7rem] lg:pt-[8rem] gap-[1.6rem] xl:gap-[2rem] pb-[2rem]"
>
{isLoading && <Spinner />}
{isError && (
<div className="text-center text-[1.4rem] text-red-500">
데이터를 불러오지 못했어요. 잠시 후 다시 시도해주세요.
</div>
)}
{!isLoading && !isError && (
<div ref={divRef} id="capture-area" className="flex flex-col space-y-[1.8rem]">
<ResultDetail
chartResult={detail?.analysisResponseDto.result}
resultType={detail?.analysisResponseDto.resultType}
description={detail?.analysisResponseDto.resultTypeDetail}
resultImage={imageName}
className="gap-2"
history
/>
</div>
)}
</SurveyLayout>
)
}
트러블슈팅
🔹 로그아웃 시 중복 토스트 알림 발생
📌 구현 배경 및 해결 전략
로그인 사용자만 접근 가능한 페이지를 보호하기 위해 루트에 있는 라우트 설정 파일에서 부모 element로서 ProtectedRoutes 을 등록함으로써, 전역상태로부터 사용자의 로그인 여부를 검사하고, 비로그인 사용자는 “로그인이 필요합니다.” 토스트와 함께 홈(/)으로 리다이렉트 시켰습니다.


그런데 로그아웃 시나리오에서 문제가 발생했습니다.
사용자가 마이페이지(/mypage)에서 로그아웃 시, 홈으로 리다이렉트되는 과정에서 “성공적으로 로그인되었다”는 토스트와 함께 “로그인이 필요하다.”는 토스트가 중복으로 뜨면서 아래와 같이 이중 토스트 현상이 나타났습니다.

📌 어떤 고민이 있었는지?
이 현상은 로그아웃이 완료되어 사용자 정보가 null로 초기화되는 시점에도, 사용자가 여전히 ProtectedRoutes에 의해 보호된 경로(/mypage)에 머물러 있기 때문에 발생하였습니다.
리액트는 상태 변경 시 관련 컴포넌트를 재렌더링하므로 /mypage에서 로그아웃으로 인해 상태 변경이 일어나면 ProtectedRoutes가 다시 실행되고, 이를 의도된 로그아웃이 아닌 인증 실패로 오인하여 로그인이 필요하다는 토스트를 중복으로 띄운 뒤 홈으로 리다이렉트된 것이었습니다. 결과적으로 사용자는 “로그아웃 직후에도 로그인이 필요하다는 안내”를 다시 보게 되며 좋지 않은 UX를 경험하게 되었습니다.
📌 어떻게 해결했는지?
ProtectedRoutes가 일반적으로 실행되어야 하는 경우와 의도된 로그아웃 상황을 식별하기 위해 Zustand에 fromSession이라는 플래그를 추가하고, 로그아웃/회원탈퇴 시점에만 이 값을 true로 설정하여 토스트 렌더링 여부를 조정 가능하도록 했습니다.
🔽 로그아웃 시, logoutMutation 함수에 의해 resetUser() 가 실행되어 fromSession 값이 false → true로 변경됩니다.

초기에는 ProtectedRoutes내부에서 다음과 같이 로그아웃 여부를 판단하고, 그 안에서 상태 초기화를 시도했습니다.

그러나 이 코드는 React에서 다음과 같은 에러를 발생시켰습니다:

이 경고는 렌더링 중 상태 변경 금지라는 React의 렌더링 원칙에 위배되었기 때문에 발생한 것으로, React에서는 렌더링 도중에 상태를 변경(setState)하는 것을 엄격히 금지하고 있는데, 이는 무한 루프, 예측 불가한 렌더링을 초래할 수 있기 때문입니다.
해당 구조에서는 로그아웃이 완료되는 시점에 user === null & fromSession === true가 되어 상태변경이 일어나면, 마이페이지를 감싸고 있던 ProtectedRoutes가 재렌더링되면서 결과적으로 이중 조건문 안의 clearFromSession()을 호출되는데, 이때 clearFromSession() 함수에 의해 fromSession의 전역 상태 변경이 발생하게 되어 오류의 원인인 "렌더링 중 상태 변경”이라는 금기 패턴이 발생한 것입니다.
따라서 아래와 같이 렌더 단계(ProtectedRoutes)에서 clearFromSession()실행을 피하고, 렌더링이 완료된 뒤 로그인 페이지의 useEffect에서 안전하게 호출하도록 수정하여 해당 경고를 해결하였습니다.


✅ 결과적으로…
- 의도된 로그아웃 흐름에서는 토스트가 없고, 미인증 상태로 보호 경로에 직접 접근했을 때만 토스트가 뜨도록 전역상태를 이용해 분기하였습니다.
ProtectedRoutes는 상태 변경을 수행하지 않고 순수한 페이지 보호 기능의 역할만 수행하여React 경고 없이 안정화시킬 수 있었습니다.
📌 무엇을 배웠는지?
- 의도된 상태 전환을 구분하는 것의 중요성을 배웠습니다. 부모 컴포넌트인
ProtectedRoutes와 자식 컴포넌트(MyPage)가 동일한 상태 변경(user = null)에 동시에 반응하면서 중복 토스트가 발생했는데, 이때 로그아웃처럼 사용자가 의도한 상태 전환을 전역 플래그(fromSession)로 구분함으로써 문제를 해결할 수 있음을 배웠습니다. - React의 렌더링 원칙을 반드시 지켜야 함을 배웠습니다. 렌더링 중 상태 변경은 금기이며, 라우팅이 완료된 최종 페이지의
useEffect에서 후처리를 수행해야 한다는 것을 배웠습니다. - 책임 분리의 필요성을 배웠습니다.
ProtectedRoutes는 검사·리다이렉트만 담당하고, 상태 초기화나 토스트 제어 같은 부수효과는 페이지 단에서 처리해야 예측 가능성이 높아진다는 것을 배웠습니다.