프로젝트 소개
- 영양제를 손쉽게 받아볼 수 있는 영양제 정기 구독 웹 서비스
프로젝트 선정 이유
- 평소에 자주 이용하는 제품이고 남녀노소 누구나 관심을 가질만한 분야인 영양제를 주제로 선정했다.
- 평범한 쇼핑몰과 차별성을 만들고 싶어서 최근 관심 있는 비즈니스 모델인 구독 기능을 도입했다.
링크
- 배포 링크: http://pillivery.s3-website.ap-northeast-2.amazonaws.com/
- 깃허브 링크: https://github.com/codestates-seb/seb40_main_033/tree/main
개발 기간
2022.11.08 - 2022.12.07
팀 구성
- Front-end 4명
- Back-end 3명
기술 스택
Front-end:
JavaScript, TypeScript, React, Styled components, Redux toolkit, React query, axios
Back-end:
Java, Spring boot, Spring Sercurity, Spring JPA, Redis, Gradle, MySQL, JWT, Quartz
Tools:
Git, Github, Figma, Postman, Discord, Zoom, Notion, Gather town
1. 팀 규칙
팀이 만들어지고 맨 처음으로 한 일은 팀 규칙 정하기였다.
팀원분들이 모두 좋은 분들이고 적극적이긴 했지만, 혹시 모르는 상황에 대비해서 팀 규칙을 정했다.
프로젝트 일반 규칙
1. 아침 9:00에 캠 키고 기상 확인하면서 인사하기
2. 데일리 스크럼 진행하기 (전 날 무엇했는지, 오늘 무엇할 건지)
3. 회의 때 인 당 1개 이상 의견 말하기 (없으면 쥐어 짜내기)
4. 상시 접속 시간에 20분 이상 자리 비울 경우, 미리 알리기
5. 오후 1시 ~ 5시 사이에 게더타운에서 진행상황 공유하며 모각코 하기
6. 쿠션어 사용, 남 탓 금지, 어?!! 금지, 하차 금지
7. 알림 사항 확인 후, 답장/이모지 필수❗️
8. 휴가/하차/병결 사전 고지❗️프로젝트 개발 관련 규칙
1. API명세서 작성할 때 FE/BE 같이 하기
2. PR 올리면 알려주고, Merge 할 때 모여서 하기, 코드 리뷰
3. 같은 파일 작업하거나, 다른 사람 파일 수정할 경우 꼭 미리 공지❗️❗️❗️❗️❗️❗️
4. 에러 1시간, 모르는 거 5시간 이상 붙잡고 있지 말기
규칙들은 원활한 커뮤니케이션에 문제가 없도록 하는 것에 초점을 맞춰서 만들었다.
특히 데일리 스크럼을 진행하도록 규칙을 정해서 온라인 프로젝트이지만 하루에 한 번은 얼굴을 보면서 대화를 나눌 수 있도록 한 점이 좋았다.
게다가 프론트엔드는 거의 대부분의 시간을 게더 타운에 모여서 같이 작업을 했기 때문에 소통에 막힘이 없어서 너무 좋았다!
2. 프로젝트 주제 선정
팀원들이 다 같이 생각나는 대로 주제들을 던지는 브레인스토밍 방식으로 주제 선정 과정을 시작했다.
아이디어를 낼 때는 안 되는 이유는 생각하지 않고 일단 말해보며 의견을 종합했고, 후에 부정적인 요소들을 생각해 보며 소거해 나갔다.
구현할 수 있는 기능들 | 우려스러운 점 | |
---|---|---|
영양제 구독 서비스 | - 원하는 주기로 영양제 받아보기 - 효능별 카테고리 분류 - 건강 정보 체크해서 필요한 영양제 추천 - 리뷰 작성 - 찜 기능 |
- 영양제 관련 지식을 공부하는 시간이 오래 걸릴 것 같음 |
여행 패키지 플랫폼 | - 여행사 패키지 / 체험 패키지 등 카테고리 분류 가격비교 - 여행객 매칭 (여행 마치고 리뷰) - 위치 별 모아보기 (서울/경기/충청/...) - 찜 기능 |
- 실제 여행사 데이터를 받아 올 수 있을까? |
이용권 중고거래 플랫폼 | - 기간을 채우지 못한 헬스장, 바디프로필 등의 이용권을 양도할 수 있도록 -촬영, 운동 등 카테고리 분류 - 회당 금액을 계산해주는 기능 - 위치 별 모아보기 (서울/경기/충청/...) - 양도비 포함 여부 표시 - 찜 기능 |
- 실 이용자가 적어서 트래픽이 작을 듯 함 - 예시가 많지 않아 참고할 자료가 부족함 - 데이터를 어디서 받아와야 할 지? |
원데이 클래스 플랫폼 | - 원데이 클래스들을 모아서 가격비교하고, 예약 - 지역별, 분야별 필터 - 리뷰 - 선물 기능 - 이달의 클래스 추천 - 사용자 맞춤 클래스 추천 (ex. mbti별) |
- 딱히 생각나지 않음 |
최종적으로 4개의 주제가 추려졌고 투표를 통해서 영양제 구독 서비스가 선정되었다.
평소에 건강에 관심이 있고 영양제도 어려 개 먹고 있어서 익숙한 주제에 투표를 했는데 선정되어서 기분이 좋았다😊
3. 사용자 요구사항 정의서
처음 사용자 요구사항 정의서를 만들 때는 온라인 쇼핑몰이라면 당연히 있어야 할 것 같은 기능들을 생각나는 대로 다 넣고 중요도를 대부분 상으로 두었다.
멘토님이 주어진 시간에 비해 기능이 많고, 중요도가 상에 밀집되어 있다고 피드백을 주셔서 반드시 필요한 기능인지 한 번 더 생각해 보고 조금씩 중요도를 낮췄다.
아무래도 기획부터 제대로 만드는 프로젝트는 처음이라서 팀 전체적으로 욕심이 많았던 것 같다.
4. User Flow
어느 정도 프로젝트의 윤곽이 잡히고 나서 프론트엔드와 백엔드 각자 작업을 시작했다. 프론트는 가장 먼저 피그잼을 사용해서 유저 플로우를 만들었다.
처음엔 사이트에 접속해서 상품을 구매하는 단방향의 유저 플로우를 만들었는데 '장바구니에만 넣는 경우', '상품이 마음에 안 들어서 다른 걸 검색하는 경우' 등 여러 가지 경우의 수들을 반영하다 보니 꽤 복잡한 유저 플로우가 만들어졌다.
유저 플로우에 시간을 상대적으로 과하게 할애한 것은 아닐까 걱정도 됐지만 프로젝트의 구조와 흐름을 팀원들끼리 합의할 수 있어서 좋았다. 그래서 나중에 피그마로 UI 작업을 할 때 사용자 요구사항 정의서와 유저 플로우가 이정표 역할을 해주어서 서로 합의했던 내용이 헷갈리면 돌아와서 볼 수 있었다.
완성을 하고 유저 플로우를 아주 세세하게 잘 만들었다고 피드백을 받아서 뿌듯했다😊
5. User Interface
와이어 프레임
보기 좋은 화면이 UX에도 좋다고 생각해서 UI 디자인에 욕심이 있었다.
먼저 팀원들 각자 레퍼런스를 찾아보고 마음에 드는 부분들을 모았다. 그리고 피그마를 사용해서 화면의 구조를 잡는 와이어 프레임을 만들었다.
와이어 프레임을 만들며 UI가 어디에 배치될지 대략적으로 파악할 수 있었다.
유저가 있다고 생각하는 곳에 자연스럽게 그 기능이 존재하도록 만드는 게 생각보다 쉽지 않은 작업이구나 느꼈다.
또, 중간중간에 백엔드 분들과 협의를 하면서 API와 화면을 유기적으로 만들려고 노력했다.
프로토타입
완성된 프로토타입을 바탕으로 프로토타입을 만들었다.
먼저 전체적인 디자인 톤을 맞추고 공통으로 사용하는 컴포넌트를 분류해서 팀원들끼리 담당 컴포넌트를 나눴다.
개발을 시작하고 디자인 때문에 막히는 일이 없이 그대로 구현할 수 있도록 모든 구성 요소들을 빼먹지 않는 데에 초점을 맞췄다.
디자인은 취향의 영역이라 의견이 다른 경우가 많았다. 그럴 때마다 최대한 레퍼런스를 바탕으로 근거 있게 설득을 하려고 했다.
꼼꼼하게 작업을 하느라 생각보다 디자인 작업이 길어져서 개발 일정에 지장이 생길 것 같자 정말 밤을 새워가며 작업 속도를 올렸었다.
완성하고 나니 각자의 취향이 잘 어우러져서 좋은 시너지 효과를 냈고, 실제 쇼핑몰로 바로 사용해도 될 만큼의 완성도가 나온 것 같아서 만족스러웠다!
개인적으로 의견을 많이 제시했는데 팀원 분들이 잘 받아주시고 또 다듬어주셔서 감사했다👍
6. 고민 사항
1) 회원가입 폼 useRef 분리
📌 해결해야 할 비즈니스 문제
- 쇼핑몰 특성상 배송을 받아야 하기 때문에 이메일, 비밀번호, 닉네임과 같은 개인정보를 제외하고 이름, 전화번호, 주소와 같은 배송지 정보가 필요
- 비밀번호 확인과 상세주소까지 합치면 총 8개의 필드를 작성해야 하기 때문에 유저 입장에선 상당히 부담스러워서 회원가입하기를 꺼릴 수 있음
📌 해결법 도출
어떻게 유저가 회원가입을 쉽게 느끼도록 만들 수 있을지 고민하다가 거꾸로 쌓이는 입력창을 도입
- 일반적으로 집중하고 있는 곳 외에 다른 곳은 잘 인지하지 못하고, 시선을 한 곳에만 둬도 되기 때문에 집중도를 올릴 수 있음
- 여러 가지 종류의 정보를 입력을 하는데서 오는 피로감을 줄일 수 있고 유저의 이탈률을 낮추는 효과가 있음
- UX를 위해 회원가입부터 로그인까지 자연스러운 포커스 이동 → 회원가입 전환율을 높임
📌 구현 과정에서 만난 기술적 문제
- React-Hook-Form 라이브러리를 사용해 회원가입 폼을 구현했고 위의 요구 사항에 따라 하나의 폼에서 2가지 애니메이션을 나눠서 트리거해줘야 했음
- 새롭게 나오는 인풋의 애니메이션
- 아래로 내려가는 인풋들의 애니메이션
- 이때 어떤 인풋에 어떤 애니메이션을 적용해야 하는지를 구분하기 위해서 useRef를 사용
- React-Hook-Form의 register에도 기본적으로 ref 값이 들어있기 때문에 input 태그에 ref 속성으로 전달해야 하는 값이 중복되는 문제 발생
const emailRef = useRef<HTMLInputElement>();
const emailRegister = register('이메일', {
required: '이메일을 작성해주세요.',
minLength: {
value: 5,
message: '이메일 형식으로 작성해주세요.',
},
pattern: {
value:
/^[0-9a-zA-Z]([-_.]?[0-9a-zA-Z])*@[0-9a-zA-Z]([-_.]?[0-9a-zA-Z])*\.[a-zA-Z]{2,3}$/i,
message: '이메일 형식으로 작성해주세요.',
},
});
//emailRegister의 Call Signature
const emailRegister: {
onChange: ChangeHandler;
onBlur: ChangeHandler;
**ref: RefCallBack;**
name: "이메일";
min?: string | number | undefined;
max?: string | number | undefined;
... 4 more ...;
disabled?: boolean | undefined;
}
📌 기술적 해결
- 애니메이션을 나눠야 하는 이슈는 전체폼에 클래스 부여해서 아래로 내려가는 애니메이션을 실행하고, 동시에 새로 나오는 인풋에는 CSS 셀렉터로 애니메이션을 부여해서 해결
- ref를 이중으로 사용해야 하는 이슈를 해결하기 위해 register에서 구조 분해 할당과 rest 연산자를 활용해서 ref를 분리 → 인풋에서 React-Hook-Form ref와 useRef로 만든 변수에 arrow function을 이용해서 input 요소의 참조를 주입
<input
ref={(e) => {
if (!e) return;
refHook(e);
refAddress.current = e;
}}
/>
2) 라우팅, 공통 레이아웃 분기 설정
📌 문제 정의
- 피그마로 프로토타입을 작업하고 나니 페이지 종류별로 다양한 레이아웃을 적용해야 했음
- e.g. 로그인/회원가입 페이지에는 아무런 레이아웃 컴포넌트가 필요 없었고, 나머지 페이지들을 배경의 구조가 각기 달랐음
- 전체 흰색인 페이지
- 흰색과 회색이 함께 있는 페이지
- 전체 회색인 페이지
📌 문제 해결
- 기본적인 라우터 구조는 react-router-dom의 Outlet을 적극적으로 활용해서 제작
- Layout 컴포넌트의 Route가 전체 페이지들을 감싸고 있고 Layout 안에 Outlet 부분에 페이지들을 렌더링
- Cart와 MyPage처럼 페이지 안에서 공통 컴포넌트가 있고 다시 분기가 일어나는 경우 이중으로 Outlet을 심기
function Router() {
return (
<BrowserRouter>
<ScrollToTop />
<Routes>
**<Route element={<Layout />}>**
<Route index element={<Home />} />
<Route path="cart" element={<Cart />}>
<Route path="normal" element={<NormalCart />} />
<Route path="subscription" element={<SubCart />} />
</Route>
<Route path="detail/:id" element={<Detail />} />
<Route path="list" element={<ItemList />} />
<Route path="login" element={<LogIn />} />
<Route path="mypage" element={<MyPage />}>
<Route path="user-info" element={<UserInfo />} />
<Route path="order/subscription" element={<SubscriptionOrder />} />
<Route path="order/normal" element={<NormalOrder />} />
<Route path="order/:id" element={<OrderDetail />} />
<Route path="sub-manage" element={<SubManage />} />
<Route path="wish" element={<WishList />} />
<Route path="note/review" element={<NoteReview />} />
<Route path="note/talk" element={<NoteTalk />} />
</Route>
<Route path="search" element={<SearchList />} />
<Route path="signup" element={<SignUp />} />
<Route path="pay/normal" element={<Payment />} />
<Route path="pay/subscription" element={<SubscriptionPayment />} />
<Route path="*" element={<NotFound />} />
**</Route>**
</Routes>
</BrowserRouter>
);
}
- 로그인/회원가입 페이지에서 레이아웃을 숨겨주기 위해 해당 페이지의 엔드 포인트를 hiddenPath라는 배열에 넣고 pathname이 변경될 때 검사하도록 구현
- 화면이 페인팅되기 전에 동기적으로 레이아웃을 확정하기 위해서 useLayoutEffect도 활용
function Layout() {
const { pathname } = useLocation();
const hiddenPath = ['/login', '/signup'];
const [isHiddenPath, setIsHiddenPath] = useState(
hiddenPath.includes(pathname),
);
const firstPathname = pathname.split('/')[1];
useLayoutEffect(() => {
setIsHiddenPath(hiddenPath.includes(pathname));
}, [pathname]);
return (
<Container pathname={firstPathname}>
<TopContainer>
{isHiddenPath || <LeftNav />}
<MainContainer isHiddenPath={isHiddenPath}>
<Outlet />
</MainContainer>
{isHiddenPath || <RightNav />}
</TopContainer>
{isHiddenPath || <Footer />}
</Container>
);
}
- 배경의 종류를 페이지별로 다르게 적용하기 위해서 pathname을 Container에 props로 전달하고 내부적으로 switch문을 활용해 분기
- 2가지 색으로 이루어진 페이지의 배경을 만들기 위해서 linear-gradient를 이용했다. 색의 중지 위치에 대한 값을 명시적으로 전달해서 뚜렷한 구분선을 만들기
const Container = styled.div<{ pathname: string }>`
display: flex;
flex-direction: column;
justify-content: space-between;
min-width: fit-content;
min-height: 100vh;
${({ pathname }) => {
switch (pathname) {
case 'cart':
case 'pay':
return `
background-color: var(--gray-100)
`;
case 'mypage':
return `
background-image: linear-gradient(to bottom, white 283px, var(--gray-100) 0)
`;
case '':
return `
background-image: linear-gradient(to bottom, white 770px, var(--gray-100) 0)
`;
default:
return `
background-color: white`;
}
}}
`;
3) 커스텀 훅을 통해 로그인 로직 추상화
- 로그인 페이지의 코드를 선언적으로 구현하기 위해서 useLogIn 커스텀 훅을 만들어 로그인에 필요한 로직을 추상화
// 로그인 페이지
function LogIn() {
const { mutate } = useLogIn();
const handleLogIn = (data: LogInFormValues) => {
mutate({ email, password });
};
return (
<AuthContainer>
<FormContainer>
<Link to="/">
<Logo />
</Link>
<AuthTitle title="로그인" />
<AuthForm handleSubmitForm={handleLogIn} />
<SocialLogIn />
<LinkContainer>
아직 회원이 아니신가요? <Link to="/signup">회원가입</Link>
</LinkContainer>
</FormContainer>
<Background>
<Text>With Pillivery Subscribe Health</Text>
</Background>
</AuthContainer>
);
}
function useLogIn() {
const navigate = useNavigate();
const dispatch = useDispatch();
const { mutate } = useMutation(
(form: LogInForm) => fetchLogIn(form),
{
onSuccess: ({ accessToken, refreshToken, userId }, { email }) => {
dispatch(login({ accessToken, refreshToken, userId, email }));
toast.success('로그인 되었습니다 !');
// 다른 페이지로 이동 후 뒤로가기 시 로그인 페이지로 이동하지 않도록
navigate('/', { replace: true });
},
onError: (data: AxiosError<{ message: string }>) => {
const { response } = data;
if (!response) return;
const { status, data: errorData } = response;
if (status === 401) {
toast.error('아이디 또는 비밀번호를 다시 확인해주세요.');
} else if (status === 403) {
toast.error('유효하지 않은 접근입니다.');
} else if (status >= 500) {
toast.error('서버가 원활하지 않습니다.');
} else {
toast.error(errorData.message);
}
},
},
);
return { mutate };
}
- email, password를 받아서 axios를 통해 post 요청을 보낸다.
- 요청 성공 시 onSuccess의 로직이 실행된다.
- 서버로부터 받아온 데이터와 요청을 보낼 때 사용했던 email을 redux store에 저장한다.
- 스낵바 메시지를 통해 로그인 여부를 유저에게 알리고 홈으로 이동시킨다.
- 이때 유저가 뒤로 가기 했을 경우 로그인 페이지로 다시 이동하지 않도록 replace를 true로 지정해서 로그인 페이지를 히스토리에서 제거한다.
- 요청 실패 시 onError의 로직이 실행된다.
- 객체 분해 할당으로 status와 errorData를 꺼내와서 각 status에 맞는 문구를 유저에게 보여준다.
- 요청 성공 시 onSuccess의 로직이 실행된다.
4) 무한 스크롤 도입으로 상품 노출도 증가 및 UX 개선
📌 문제 정의
- 쇼핑몰 특성상 목록 페이지에서 여러 개의 아이템을 사진과 함께 받아와야 하기 때문에 로딩 시간이 길어질 수 있다고 판단
- 이를 개선하기 위해 처음엔 페이지네이션을 사용
→ 다음과 같은 이유에서 무한 스크롤이 더욱 적합하다고 생각되어 마이그레이션 함- 스크롤 기반 탐색으로 유저가 더 많은 상품을 자연스럽게 탐색하여 상품의 노출도 증가
- 페이지 전환 요청을 위해 버튼을 클릭하는 사용자의 추가 조작을 없애서 UX 개선 가능
- 이를 개선하기 위해 처음엔 페이지네이션을 사용
📌 문제 해결
- 스크롤 이벤트로 무한스크롤을 구현하는 방법은 브라우저 reflow를 과하게 발생시켜 메인 스레드에 영향을 주어 성능 저하 유발
- 따라서 비동기적으로 실행되어 메인 스레드에 영향을 주지 않고 변경 사항을 관찰할 수 있는 Intersection Observer API 사용
function ItemList() {
const { ref, inView } = useInView();
const { data, status, fetchNextPage, isFetchingNextPage } =
useGetList({ pathname, category, path, query });
useEffect(() => {
if (inView) fetchNextPage();
}, [inView]);
return (
<Box>
<Brand>
<BrandsWindow />
</Brand>
<ItemListBox>
{data?.pages.map((page, index) => (
<React.Fragment key={`${index.toString()}`}>
{page.data.map((item) => (
<SmallListCards key={item.itemId} item={item} />
))}
</React.Fragment>
))}
</ItemListBox>
**{isFetchingNextPage ? <LoadingSpinner /> : <div ref={ref} />}**
</Box>
);
}
최하단 div가 화면에 노출되면 inView 상태가 true가 되어 다음 페이지를 fetch 하도록 구현
7. 타입스크립트 리팩터링
💡 타입스크립트 도입 시 모든 파일을 한 번에 바꿀 수 없기 때문에 tsconfig.json에서 `"allowJs": true,`를 설정하여 자바스크립트 코드와 혼용이 가능하도록 설정
1) type predicates + type narrowing으로 타입 안정성 향상
📌 문제 정의
로그인과 회원가입 페이지의 폼 컴포넌트를 하나로 통일해서 사용하고 있었기 때문에 폼이 submit 됐을 때 각 페이지로 넘어오는 데이터의 타입은 유니온 타입임
// submit 되면 실행되는 함수.
const onValid = (data: AuthFormValues) => {
handleSubmitForm(data);
};
export interface LogInFormValues {
이메일: string;
비밀번호: string;
}
export interface SignUpFormValues extends LogInFormValues {
비밀번호확인: string;
닉네임: string;
주소: string;
상세주소: string;
우편번호: string;
전화번호: string;
이름: string;
}
export type AuthFormValues = LogInFormValues | SignUpFormValues;
각 페이지에서 API 요청을 할 때 어떤 타입으로 보낼지 타입을 좁힐 필요가 있었음
📌 문제 해결
폼을 전송하는 함수를 가진 상위 컴포넌트에서 type predicates(is)을 통해 react query의 mutate 함수로 전달하는 데이터의 타입을 좁힘(type narrowing)
const isSignUp = (form: AuthFormValues): form is SignUpFormValues =>
'비밀번호확인' in form;
- 비밀번호확인 필드를 가지고 있으면 SignUpFormValues 타입이라는 뜻이 됨
- isSignUp 함수를 통과하고 나온 데이터는 무조건 SignUpFormValues 타입
const handleSignUp = (data: AuthFormValues) => {
if (isSignUp(data)) mutate(data);
};
2) useRef 타입 오버로딩 파악
📌 문제 정의
- 로그인, 회원가입 폼에 애니메이션을 트리거하기 위해서 만든 useRef에 타입을 줘야 했음
- 처음엔 아래와 같은 형태로 타입을 부여했었지만 인풋의 ref 속성으로 부여된 useRef 값의 current에 접근할 수 없는 문제가 생김
const inputRef = useRef<HTMLInputElement>(null);
📌 원인
- useRef 훅은 3개의 정의가 오버로딩되어 있음
useRef<T>(initialValue: T): MutableRefObject<T>;
인자의 타입과 제네릭의 타입이 T로 일치하는 경우, MutableRefObject를 반환
MutableRefObject<T>의 경우, current 프로퍼티 그 자체를 직접 변경할 수 있음useRef<T>(initialValue: T|null): RefObject<T>;
인자의 타입이 null을 허용하는 경우, RefObject<T>를 반환
RefObject<T>는 current 프로퍼티를 직접 수정할 수 없음useRef<T = undefined>(): MutableRefObject<T | undefined>;
제네릭의 타입이 undefined인 경우(타입을 제공하지 않은 경우), MutableRefObject<T | undefined>를 반환
- 각 타입의 정의
interface MutableRefObject<T> {
current: T;
}
interface RefObject<T> {
readonly current: T | null;
}
- MutableRefObject은 current가 수정 가능하지만 RefObject는 readonly가 붙어있어서 불가능
- 즉 초기값으로 null을 보냈기 때문에 current에 접근이 안 됐음을 파악
📌 문제 해결
3번 오버로드를 사용하는 것으로 해결
const inputRef = useRef<HTMLInputElement>();
초기값을 비워서 undefined로 타입을 부여하면 뮤터블 하게 사용이 가능해짐
3) 이벤트 핸들러가 바인딩된 요소의 타입 추론
📌 문제 정의
타입스크립트 도입 전엔 e.target으로 innerText에 접근했었음
const handlePeriodClick = (event) => {
setPeriod(Number(e.target.innerText.replace('일', '')));
patchPeriod();
};
하지만 e.target의 기본 타입이 EventTarget이기 때문에 타입스크립트 도입 후 innerText 속성에 접근할 수 없는 오류 발생
📌 해결방법 1) event.target
을 HTMLLIElement
로 캐스팅
e.target의 타입이 HTMLLIElement로 캐스팅되어 innerText 속성에 접근 가능
const handlePeriodClick = (event: React.MouseEvent<HTMLLIElement>) => {
const target = event.target as HTMLButtonElement;
setPeriod(Number(target.innerText.replace('일', '')));
patchPeriod();
};
📌 해결방법 2) event.currentTarget
를 사용
currentTarget은 이벤트가 발생한 요소가 아니라 이벤트 핸들러가 바인딩된 요소를 가리키기 때문에, 타입 추론이 가능
const handlePeriodClick = (event: React.MouseEvent<HTMLLIElement>) => {
setPeriod(Number(event.currentTarget.innerText.replace('일', '')));
patchPeriod();
};
개발자가 타입을 강제하는 타입 단언 사용을 피하기 위해 currentTarget을 선택
8. KPT 회고
KEEP
- 팀원들과의 소통
- 매일 아침 데일리 스크럼을 진행하며 각자의 진행 상황과 요구 사항을 취합할 수 있었다.
- 최대한 편안한 분위기에서 자유롭게 의견을 제시할 수 있도록 분위기를 조성하려 노력했다.
- 타당하고 근거 있는 의견 제시와 수렴
- 의견을 조율해야 할 때는 타당성 있는 근거를 제시해서 설득했다.
- 팀원의 의견이 더욱 타당하거나 서로의 중간 지점에 더 좋은 해결 방안이 있으면 유연하게 의견을 수정했다.
- 해결하기 어려운 문제를 서로 공유
- 다른 분들이 작업한 컴포넌트를 사용할 때 오랫동안 막히면 문제를 공유해서 작업 시간을 단축했다.
- 담당 구현을 가장 먼저 끝내고 시간이 부족한 팀원의 분량을 도와서 마감 기한을 지켰다.
PROBLEM
- 일정 관리 미흡
- 초반에 욕심이 많아서 프로젝트 사이즈를 크게 기획했고 작업을 하면서 중요도가 낮은 기능을 덜어내야 했다.
- 기획부터 디자인과 개발까지 진행한 프로젝트는 처음이어서 일정 관리가 쉽지 않았기 때문에 후반부에 시간이 촉박했다.
- 후반부에는 팀원의 코드를 꼼꼼히 리뷰하지 못함
- 프로젝트 후반부에 구현이 미흡한 부분에 신경 쓰느라 다른 분들의 코드를 꼼꼼하게 리뷰하지 못했다.
TRY
- 프로젝트 일정 관리 개선
- 초기 기획 단계에서 중요도와 우선순위를 고려하여 프로젝트의 사이즈를 적절하게 조절하자.
- 프로젝트 일정을 세분화하여 관리하고, 주기적으로 일정을 점검하고 조정하여 후반부에 시간적 여유를 만들자.
- 코드 리뷰 강화
- 시간을 효율적으로 배분하여 코드 리뷰를 꼼꼼히 진행하고, 팀원 간의 피드백을 적극적으로 수렴하여 코드 퀄리티를 개선하자.
- 팀원 간의 역할 분배 및 협업 강화
- 팀원 간의 역할 분배를 명확하게 하여 각자의 작업에 집중할 수 있도록 돕자.
- 서로의 작업이 원활하게 진행될 수 있도록 의사소통을 더욱 활발하게 하고, 진행 상황을 지속적으로 공유하자.