Search

안녕하세요, 3년차 프론트엔드 개발자 정동환입니다.

프로젝트

PillME

2025.01 ~ 2025.02 (PM 1,DESIGN 1, FE 2, BE 1)
연합동아리 CMC에서 영양제 추천 서비스를 주제로 진행한 팀 프로젝트 - 16기 우수상 수상
React, React Native, TypeScript, Vite, Vanilla Extract
내가 기여한 부분
Chromatic 기반 시각 회귀 테스트 & UI Review 자동화
Input, Search-field, Chip, Switch, Checkbox 등의 공통 컴포넌트 개발 및 Storybook 문서화
typedoc 기반 TypeScript 문서화 자동화
queryOptions API로 query Factory 패턴을 적용해 쿼리 키와 옵션을 도메인별로 관리
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의 엣지 캐시와 브라우저 캐시가 이전 번들을 계속 서빙하고 있었기 때문이었습니다.
해결 방법 앱 단에서 "pull-to-refresh" 제스처로 사용자가 직접 최신화할 수 있도록 했습니다. PR
해당 경험을 통해 알게 된 점
정적 호스팅 환경에서는 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