프로젝트
연합동아리 CMC에서 영양제 추천 서비스를 주제로 진행한 팀 프로젝트 - 16기 우수상 수상
React, React Native, TypeScript, Vite, Vanilla Extract
내가 기여한 부분
•
Chromatic 기반 시각 회귀 테스트 & UI Review 자동화
•
Input, Search-field, Chip, Switch, Checkbox 등의 공통 컴포넌트 개발 및 Storybook 문서화
•
typedoc 기반 TypeScript 문서화 자동화
•
•
motion으로 페이지 등장, 전환 애니메이션을 적용해 네이티브 앱과 비슷한 UX 제공
•
React-native 앱과의 브릿지 구현(외부 링크 열기, 구매링크 복사 등)
•
Vaul 기반으로 바텀 시트를 개발해 약관 동의 / 카테고리 필터링 등에서 공통 사용
회원가입 퍼널
•
기존 회원가입 퍼널에 유저 정보 입력 스텝(이름, 생일, 성별) 및 약관 동의 바텀시트 추가
◦
react-hook-form의 formContext로 스텝 간 상태를 공유하고 zod로 스텝별 유효성 검사를 처리
•
회고
◦
회원가입 퍼널에서 하나의 폼으로 관리하고 useFormContext로 전역 폼 상태를 공유하는 구조를 택했지만 실제로는 각 스텝이 자기 필드만 register하는 수준으로만 사용되고 스텝 간 상태 전달은 이미 @use-funnel의 history context가 담당하고 있었다. 각 스텝마다 독립된 푬을 사용하는 방식이 더 좋다고 생각
홈 / 온보딩
온보딩 캐러셀
홈 화면 캐러셀
홈 화면
•
Embla Carousel을 활용해 온보딩 및 홈 화면의 캐러셀을 구현
◦
온보딩 화면은 자동 재생으로 시작하되 사용자가 터치한 이후에는 수동 전환 방식으로 변경되어야 했고 홈 화면은 처음부터 끝까지 수동 조작만 허용되는 요구사항
◦
Embla Carousel은 공식 문서에 다양한 예제가 제공되어 코드 생성과 커스터마이징이 용이했고 패키지 용량이 작으며 npm trends에서도 높은 선택을 받고 있어 도입
•
이미지 화질, 용량 최적화 - Retina 디스플레이 대응
◦
Figma에서 export한 이미지가 모바일 Retina 디스플레이에서 흐릿하게 표시되는 문제
◦
Figma에서 2x 해상도로 export해서 해결하고 4배로 증가한 파일 용량은 PNG → WebP 변환으로 최적화
◦
온보딩 캐러셀에서 사용하는 4개 이미지 총 용량 139.1KB (PNG 1x) → 104.36KB (WebP 4x)
▪
해상도를 4배로 키우면서 오히려 용량이 약 25% 감소
카테고리 검색, AI 검색
카테고리 검색 기능
gpt api를 이용한 ai 검색 기능
내 약통(PillBox) 관리, 추가 페이지, 장바구니 페이지
내 약통 관리 페이지
내 약통 추가 페이지
장바구니 페이지
•
내 약통에서 삭제기능 구현 시
◦
결제, 인증과 무관한 도메인이라 실패 시 롤백 비용이 낮다고 판단해 Optimistic update 적용
◦
백엔드와의 의사소통으로 단건 삭제 API를 다건 삭제 API로 변경해 API 호출 횟수 감소
마이페이지 / 내 정보 관리
마이 페이지
내 정보 관리 페이지
•
닉네임, 생년월일 수정 모달의 오픈 여부를 useReducer로 관리
◦
모달이 2개 이상이어서 상태를 한 곳에서 중앙제어
PWA
•
웹에서 앱처럼 설치, 사용 가능하도록 Open Graph, Twitter Card, 아이콘 등 메타데이터 등록
트러블 슈팅
1. S3 + CloudFront 배포 환경에서 최신 버전이 반영되지 않는 캐시 이슈
문제 배경
서비스를 AWS S3 + CloudFront로 배포하는 구조였는데 새 버전을 배포한 뒤에도 사용자에게 이전 버전이 보이는 문제가 있었습니다. CloudFront의 엣지 캐시와 브라우저 캐시가 이전 번들을 계속 서빙하고 있었기 때문이었습니다.
해당 경험을 통해 알게 된 점
•
정적 호스팅 환경에서는 HTTP 캐시 무효화 전략을 고려해야 하는 점
•
웹, 앱 하이브리드 구조에서는 "웹 단독으로는 까다로운 문제"를 앱 단의 얇은 래퍼로 쉽게 풀 수 있다는 점
2. 같은 상품이 내 약통에 중복 추가되는 이슈
문제 배경
이미 내 약통에 있는 상품을 다시 추가할 때 기존 항목의 수량이 증가하는 게 아니라 같은 상품이 별개 항목으로 누적되는 문제가 있었습니다. 더불어 삭제 API는 상품 id 배열을 받는데, 이 리스트에 중복 id가 포함될 가능성이 있었습니다.
해결 방법
두 가지 방향으로 나눠서 해결했습니다.
1.
백엔드에 같은 상품 추가 시 수량 증가가 맞는 정책임을 재확인하고 백엔드 수정을 요청해 반영
2.
백엔드에 "삭제 API요청 인자인 id 배열에 중복 id가 있어도 되는지"를 확인 "중복 제거 후 보내 달라"는 답변을 받아 프론트에서 Set으로 중복 제거 후 요청 body를 구성
mutate({ myMedicineIds: Array.from(new Set(selectedItemIdList)) });
TypeScript
복사
해당 경험을 통해 알게 된 점
API 스펙 문서만으로 모호한 부분은 백엔드 개발자와 직접 소통하는 게 가장 빠른 해결 방식이고 혼자 가정하고 진행하면 나중에 양쪽 모두 추가 작업이 발생할 수 있다는 점
프로젝트로 알게된 점
•
앱스토어에서는 소셜 로그인을 도입하려면 애플 로그인이 필수. 일반 로그인만 있는 경우는 해당하지 않지만 구글, 카카오 같은 소셜 로그인이 하나라도 있으면 애플 로그인도 병행해야 함
•
모니터링 도구를 앱에 탑재하려면 사용자 고지가 필요하고 고지 없이 도입하면 심사에서 거부될 수 있음
•
앱 썸네일, 설명에 표기한 기능은 실제 앱에 모두 구현되어 있지 않으면 심사에서 거부될 수 있음
2023.01 ~ 2023.02(FE 2, BE 3)
현대자동차 소프티어 부트캠프에서 자동차 SNS를 주제로 진행한 팀 프로젝트
TypeScript, Axios, sass
내가 기여한 부분
•
Component Class의 setState 호출 시 prev/next state를 얕은 비교하여 불변 시 render 호출을 스킵하는 로직 추가
•
팔로워 / 팔로잉 삭제 시 상단의 팔로워 수가 갱신되지 않던 문제를 해결하기 위해 상태를 공통 조상 컴포넌트로 끌어올려서 해결
•
글 상세 페이지 전반 구현 (이미지 원본 모달 뷰어, 좋아요/ 좋아요 취소, 게시글 삭제 및 수정/삭제 드롭다운)
•
CSS Transition 으로 경고 토스트 FadeInAndOut 애니메이션, 좋아요/취소 Scale 애니메이션 개발
•
회원가입, 로그인, 회원정보 수정 폼 유효성 검사
•
회원가입 / 로그인 / 프로필 / 글 상세 페이지
트러블 슈팅
문제 배경
자체 제작한 Component 클래스에서 setState → render → setEvent 가 템플릿 메소드 패턴으로 자동 실행되는 구조였습니다.
// core/Component.ts
render() {
this.$target.innerHTML = this.template();
this.mounted();
}
setState(newState: object) {
this.state = { ...this.state, ...newState };
this.render();
this.setEvent(); // 매 렌더마다 호출
}
TypeScript
복사
이벤트 위임을 위해 상위 DOM 엘리먼트(this.$target)에 핸들러를 등록하고 있었는데 this.$target 은 innerHTML 로 자식만 교체될 뿐 자기 자신은 재생성되지 않기 때문에 리렌더링될때마다 동일한 DOM 노드에 같은 클릭 핸들러가 누적되는 문제가 발생했습니다.
해결 방법
setEvent(): void {
if (this.$target.classList.contains("once")) return;
this.$target.classList.add("once");
this.$target.addEventListener("click", (e) => { ... });
}
TypeScript
복사
우선 DOM 자체에 once 클래스를 추가하고 이미 클래스가 있으면 Early return 함으로써 해결했습니다.
그런데 /profile/A → /profile/B 로 이동한 뒤 B 화면의 탭을 클릭하면 A 의 데이터가 갑자기 다시 나타나는 버그가 발생했습니다.
SPA 라우터가 같은 공유 루트를 재사용 하면서 이전 인스턴스가 심어둔 once 클래스와 핸들러가 $target 에 그대로 남아있었고 새 인스턴스는 once 를 보고 이벤트 바인딩을 건너뛰었습니다.
결과적으로 B 화면에서 클릭이 발생하면 $target 에 붙어있는 A의 핸들러가 동작해 A.setState → A.render → A.$target.innerHTML = A.template() 로 이어지며 화면을 A로 덮어씌웠습니다.
setEvent(): void {
const profileContainer = this.$target.querySelector(
".profile__container"
) as HTMLElement;
profileContainer.addEventListener("click", (e: Event) => {
// target.closest(...) 분기
});
}
TypeScript
복사
이 문제는 라우터에 의해 계속해서 재사용되는 $target 대신 render 함수가 실행될 때마다 새로 생성되는 하위 엘리멘트에 이벤트를 등록함으로써 해결했습니다.
해당 경험을 통해 알게된 점
이벤트 위임 패턴은 핸들러를 등록하는 DOM 노드의 "수명" 까지 함께 고려해야 한다는 점을 배웠습니다. 자식만 교체되는 부모 노드에 렌더할때마다 이벤트 핸들러를 등록하면 렌더마다 누적됩니다.
수상 경력
•
DND 12기 대상
•
CMC 16기 우수상
•
2023 MetLife HACK4JOB 해커톤 우수상
•
2021년 동국대학교 비대면시대의 인공지능 챌린지 우수상
참여했던 교육 활동 및 강의
•
함수형 프로그래밍과 JavaScript ES6+ - 유인동
•
함수형 프로그래밍과 JavaScript ES6+ 응용편 - 유인동
•
실무에 바로 적용하는 프런트엔드 테스트 - 1부. 테스트 기초: 단위・통합 테스트 - 코드 조커, 오프
•
클린코드 자바스크립트 - Poco Jang
•
클린코드 리액트 - Poco Jang
•
프론트엔드 필수 브라우저 101 - 드림코딩
•
TDD와 리액트 테스트 - 드림코딩
•
포트폴리오 웹사이트 클론코딩 - 드림코딩
•
타입스크립트 - 객체지향 프로그래밍 - 드림코딩
•
리액트 개념 정리 - 드림코딩
•
Next.js 개념 정리 - 드림코딩
•
2022 삼성 SDS 하계 대학생 알고리즘 교육
업데이트 날짜: @4/7/2026











