안녕하세요 프론트엔드 개발자 Garden, 오소현입니다:)
저는 요즘 항해 플러스 프론트엔드 3기 코스 과정에 참여하면서 공부하고 있는데요!
오늘은 그 5주차의 회고를 진행해보려고 합니다 !
저번주차에 이어서 테오 코치님의 클린코드 파트가 진행되고 있었는데요!
지난 주차에는
게슈탈트 법칙으로 이해하는 클린코드: 가독성의 비밀 이 테오코치님의 블로그글을 함께 읽으면서
단일 책임의 원칙, 기능을 드러내는 함수명 짓는 방법, 더 나은 코드 구조 설계 등등을 학습했었습니다:)
하지만 이번주에는 "디자인 패턴과 함수형 프로그래밍"이라는 발제를 주제로 과제가 진행되었는데요! 한 주동안 함수형 프로그래밍을 학습하면서, 액션-계산-데이터를 분리하며 함수형으로 개발하는 것에 몰입했었고, 또한 제가 구현한 커스텀 훅과 유틸함수에 대해 테스트 코드를 작성해보는 시간을 가지기도 했습니다
저번주차에도 제가 가장 힘든 주차였다고 말씀을 드렸었는데, 이번주차도 며칠을 밤을 샜던 것 같아요,,,,
이번 과제에서 컴포넌트를 분리할 때 UI 그리는 컴포넌트
와 데이터를 전달 받아 그리는 컴포넌트
를 기준으로 분리하고자 했습니다.
UI를 그리는 컴포넌트는 따로 📂shared-ui 라는 폴더로 분류하여 그 안에서 atoms과 organisms으로 공용 컴포넌트를 분리했습니다.
데이터를 전달 받아 그리는 컴포넌트는 각각의 도메인으로 폴더를 분류하여 📂Admin(관리자 페이지) 📂 User(장바구니 페이지)로 구분해 컴포넌트들을 배치하였습니다. 따라서 현재 프로젝트는 아래의 사진과 같은 구조를 가지고 있습니다.
UI Component : UI만 그리는 경우 / Data Component : 데이터를 다루는 경우 / Composition : 앞선 두개를 조합한 경우
컴포넌트를 분리할 때 유사하게 생긴 UI라고 판단해 공용 컴포넌트로 개발할까?
라는 고민을 했었습니다.
아래의 사진을 바탕으로 판단해보면, 두 개의 컴포넌트는 각각 받아와서 보여지는 데이터가 다르기에 이를 분리해서 개발하는게 맞다고 생각했습니다. 그 이유는 하나로 개발했을 경우 Props가 확장될 것 같고(조건부) 추후 컴포넌트에 다른 기획이 추가 되었을때 확장하기 어려운 구조라고 판단했기 때문입니다.
Data component를 생성하면서 각각에 필요한 Button, Input, TitleText 등등은 UI 컴포넌트를 조합해서 만들어지도록 구현했습니다.
흔히 생각하는 추상화된 디자인 시스템 구조로 만들어지게 되었는데, 그렇다 보니 Button 컴포넌트는 서비스 내 모든 유형의 버튼을 뒷받침하고 있는 구조여서 옵션이 많아지게 되었습니다. Button 하나만을 두고 여러 상황을 수용하는 구조가 맞을 지, WideButton 이렇게 구분하는 게 좋은 구조 일지 궁금했었습니다!
위의 사진처럼 이번 제 과제의 각각의 데이터 컴포넌트는 UI 컴포넌트 요소들을 조합하여 생성되었습니다.
이런 구조로 UI 컴포넌트와 Data 컴포넌트를 만들어 나가는게 맞을 지 궁금했었어요 :)
이번 과제를 진행하며 가장 고민한 주제 중 하나였던 액션과 계산, 데이터
를 어떻게 분리하며 리팩토링해 나아갈까? 였습니다.
저희 팀원분들과 함께 생각해본 초반의 정의는 다음과 같았는데요!
- 훅 -> 데이터, 액션 함수로 이루어진 게 아닐까?
- 유틸 -> 계산, 순수 함수만 가져가는 구조로 두는게 아닐까?
개발자가 수정하고 싶을때 는 훅, 즉 액션들을 보고 뭐가 봐뀔지 확인하는 것이다.
로직들, 즉 비즈니스 로직은 유틸안에 계산 함수를 보면서 확인 하는 것이다.
준일 코치님께서 멘토링해주신 내용을 바탕으로 저는 계산 함수를 생성할 때 아래의 조건을 만족해야한다고 생각했습니다.
따라서 주어진 로직을 커스텀 훅과 계산함수로 분리하는 기준을 위의 두 기준으로 생각하며 분리하였는데 제가 잘 분리한게 맞을 지 궁금했었습니다!
계산 함수 살펴보기
계산 함수에 대한 테스트 코드 살펴보기
저는 주로 테스트 코드를 작성하면서 테스트 이름을 지을때 해당 테스트의 예상되는 기댓값을 작성해주는 편입니다.
예를들어 ~~~ 하면, ~~~ 이래야 한다.
와 같은 형식로 테스트 코드의 이름을 짓습니다.
test('수량이 0으로 설정된 경우 항목을 제거해야 합니다.', () => {
const updatedCart = cartUtils.updateCartItemQuantity(cart, '1', 0);
expect(updatedCart.length).toBe(1);
expect(updatedCart[0].product.id).toBe('2');
});
위와 같은 테스트 코드의 경우가 그 예시인데 다른 분들의 적절한 테스트 네이밍을 짓는 법이 궁금합니다 ㅎㅎ
제가 생성한 커스텀 훅은 물론, 일반 계산 함수들에 대해서도 테스트 코드를 작성했습니다. 이 주제로 해당 토픽을 본격적으로 시작해보겠습니다!
커스텀 훅 테스트 코드를 작성할 때는 해당 훅이 실행될 때 의도한 함수 순서대로 실행하는 방법으로 테스트 코드를 작성하였습니다.
test('새 할인 정보가 추가되면 제품 정보에 반영되어야 하고, 업데이트 콜백이 호출되어야 한다.', () => {
const { result } = renderHook(() => useManageProducts(mockProps));
const newDiscount = { quantity: 3, rate: 8 };
//handleEditProduct 함수 실행
act(() => {
result.current.handleEditProduct(mockProduct);
});
//setNewDiscount 함수 실행
act(() => {
result.current.setNewDiscount(newDiscount);
});
//handleAddDiscount 함수 실행
act(() => {
result.current.handleAddDiscount(mockProduct.id);
});
const expectedProduct = {
...mockProduct,
discounts: [newDiscount],
};
expect(mockProps.onProductUpdate).toHaveBeenCalledWith(expectedProduct);
//handleRemoveDiscount 함수 실행
act(() => {
result.current.handleRemoveDiscount(mockProduct.id, 0);
});
expect(mockProps.onProductUpdate).toHaveBeenCalledWith({
...mockProduct,
discounts: [],
});
});
위와 같은 방식으로 커스텀 훅을 테스트 할때 올바르게 테스트 코드를 작성한 것인지 궁금합니다!
//구현한 일반 계산 함수
/**
* @function applyCouponDiscount
* @description 총액에 쿠폰 할인을 적용
* @param {number} total - 쿠폰을 적용하기 전의 총액
* @param {Coupon | null} coupon - 적용할 쿠폰
* @returns {number} 쿠폰이 적용된 후의 총액
*/
export function applyCouponDiscount(total: number, coupon: Coupon | null) {
if (!coupon) return total;
const { discountType, discountValue } = coupon;
return discountType === 'amount'
? Math.max(0, total - discountValue)
: total * (1 - discountValue / 100);
}
위와 같은 계산 함수를 대상으로 테스트 코드를 작성할 때 발생할 수 있는 다양한 상황에 대해서 테스트 코드를 작성했던 것 같습니다.
✓ applyCouponDiscount 함수 테스트
✓ 금액 쿠폰이 적용될 경우 할인 후 금액이 계산되어야 한다.
✓ 퍼센트 쿠폰이 적용될 경우 할인 후 금액이 계산되어야 한다.
✓ 쿠폰이 null일 경우 할인이 적용되지 않고 원래 금액이 반환되어야 한다.
✓ 할인 금액이 총액을 초과할 경우 할인 후 금액은 0이어야 한다.
✓ 퍼센트 할인이 100%일 경우 할인 후 금액은 0이어야 한다.
이러한 시나리오가 생각되면서 예외 처리를 해주기도 했는데, 이런 식으로 함수를 다시 고쳐나가면서 테스트 코드를 작성해도 되는지
또한 제가 해당 함수에 대해 생각한 시나리오가 계산 함수를 테스트 하기에 적절한 시나리오인지 궁금하기도 했어요!
저는 Topic 3을 구현해보면서 관리자 페이지에서 등록한 쿠폰을 가지고 유저 페이지에서 잘 등록해 사용이 가능한 걸 테스트 할 수 있었으면 좋겠다고 생각해 자연스레 평소 관심사이기도 했던 E2E로 한번 구현해보자! 하고 이번 주차에 도전했습니다 💪🏻 (준일 코치님 께서 멘토링때 용기를 주셔서 ㅎㅎ 감사했습니다)
따라서 다음과 같은 유저 행동 기반 시나리오를 생각해보고 코치님께서 추천해주신 Playwright를 사용해 구현해보게 되었습니다.
서비스 페이지 로드가 잘 되는지 ?
1) 사용자에게 웹서비스가 정상적으로 로드 되어야한다.
상품을 추가하면 결제 금액이 기대한 값으로 잘 보이는 지 ?
2) 사용자가 장바구니에 상품을 추가하고 쿠폰을 적용하여 최종 결제 금액을 확인할 수 있어야 한다.
다양한 장바구니에서 발생할 수 있는 시나리오가 올바르게 실행되는지 ?
3) 사용자가 장바구니에서 상품 수량을 조절하고 상품을 삭제하여 최종 결제 금액이 정확하게 반영되는지 확인할 수 있어야 한다.
상품 추가가 잘 보이는지 ?
4) 관리자가 새로운 상품을 추가하면 상품 목록에 해당 상품이 정상적으로 표시되어야 한다.
쿠폰 추가한게 잘 보이는 지 ?
5) 관리자가 새 쿠폰을 등록하면 쿠폰 목록에 쿠폰이 정상적으로 표시되어야 한다.
관리자 페이지에서 등록한 게 유저 페이지에서 잘 보이고, 올바르게 적용되는지 ?
6) 관리자가 등록한 새 쿠폰을 장바구니 페이지에서 적용할 때 쿠폰에 따른 할인이 정확하게 반영되어야 한다.
제가 적절하게 시나리오를 잘 생각했는지 궁금했었습니다 ㅎㅎ
저는 e2e 테스트를 구현할 때는 주로 Given(사전 준비) - When(실행) - Then(검증)
구조를 기반으로 테스트 코드를 작성합니다.
예시로 한번 제 테스트 코드를 가져왔습니다
test('관리자가 등록한 새 쿠폰을 장바구니 페이지에서 적용할 때 쿠폰에 따른 할인이 정확하게 반영되어야 한다.', async ({
page,
}) => {
// Given :: 테스트 시작 페이지로 이동
await page.goto('http://localhost:5173/index.refactoring.html');
// Given :: 관리자 페이지로 이동
await page.locator('button', { hasText: '관리자 페이지로' }).click();
// Given :: 쿠폰 정보를 입력
await page.locator('input[placeholder="쿠폰 이름"]').fill('1000원 할인 쿠폰');
await page.locator('input[placeholder="쿠폰 코드"]').fill('AMOUNT1000');
await page.locator('select').selectOption({ label: '금액(원)' });
await page.locator('input[placeholder="할인 값"]').fill('1000');
// Given :: 쿠폰을 추가
await page.locator('button', { hasText: '쿠폰 추가' }).click();
// Then :: 쿠폰이 정상적으로 추가되었는지 확인
await expect(page.locator('[data-testid="coupon-3"]')).toHaveText(
'1000원 할인 쿠폰 (AMOUNT1000):1000원 할인'
);
// When :: 테스트의 핵심 행동 체크 (페이지 이동, 장바구니에 추가해 쿠폰이 선택되는지
await page.locator('button', { hasText: '장바구니 페이지로' }).click();
await page.locator('[data-testid="product-p1"] button', { hasText: '장바구니에 추가' }).click();
await page.locator('select[name="coupon-select"]').selectOption({ index: 2 });
// Then :: 최종 결제 금액이 예상대로 나타나는지 확인
await expect(page.locator('text=최종 결제 금액: 9,000원')).toBeVisible();
});
시나리오 동작에 대한 테스트 코드를 작성할때 위와 같은 구조로 작성해나가는게 좋은 구조인지 ? 더 좋은 방법이 있을지 피드백을 받고 싶었어요!
이번주차 과제는 테오코치님께서 전체 QA때 "뇌절할만큼 나눠보시져!"라고 말씀하셔서 컴포넌트를 분리하는게 정말 어려웠어요 ㅠㅠ 파일도 많으니까 많이 헷갈리고 원본과 많이 비교하면서 놓치는 부분 없이 꼼꼼하게 챙기는게 가장 신경 쓴 부분이었습니다.
또한 Playwrigh로 평소에 관심있던 e2e 테스팅까지 같이 구현해보면서 도전적인 과제를 진행해서 더 몰입해서 공부할 수 있었던 한주였습니다
이번주 멘토링은 준일 코치님께 받았는데요!
1주차때뵙고 오랜만에 멘토링을 받게 되어 정말 설렜고, react와 함수형 프로그래밍을 위주로 멘토링을 받았습니다!
우선 저희 팀 내에서
- 액션과 계산의 개념이 정확히 무엇일까?
액션: input에 대한 output이 달라질 수 있는 것들
계산: input에 대한 output이 항상 똑같은 것들 (함수의 인자를 통해서만 작업을 수행하는 것)
데이터: 계산이나 액션에 쓰이는 값. 어플리케이션의 다양한 상태 (엔티티 데이터, 뷰 데이터)
위로 정의해서 설명해 주셔서 정말 잘 이해할 수 있었습니다 ㅎㅎ
- 엔티티 → 리파지토리 → 유즈케이스 → UI 계층의 예시
엔티티를 레포지토리에서 가져와서 추가/수정/삭제/조회 할 수 있다.
레포지토리의 있는 추가/수정/삭제/조회의 기능을 이용해서 유스케이스 기반의 복잡한 비즈니스 로직을 만들 수 있다.
유즈 케이스를 기반으로 custom hook을 만들고, custom hook을 이용하여 ui에 변화를 줄 수 있다.
둘다 PASS와 Best Practice를 받았네요..! 정말 감사합니다!
이번주에는 추가 요구사항이 상태관리가 있었는데 저는 상태관리를 도전하기에 아직 모르는게 많아서 도전을 해보다가 시간상 그만 두게 되었었어요 ㅎㅎ,, 이점이 부족했던 것 같아요! 더 도전해보기!!
그리고 제시된 테스트 코드로만 테스트 할 수 없었던 e2e 테스트를 구현해봤던 게 가장 칭찬할 점이었고, 더 도전적으로 과제에 임했으면 좋겠습니다 :)
저는 현재 5주차를 진행하고 있지만 3주동안 정말 열심히 몰입해서 공부하고 있는데요!
바로 다음 기수인 항해 플러스 프론트엔드 코스 4기를 모집하고 있다고 하여 공유드립니다!
현재는(24.10.26)에는 슈퍼 얼리버드 기간으로 약 46% 할인을 받을 수 있습니다!
저도 3기 입과할때 슈퍼 얼리버드 기간에 합류해 추천인 할인까지해서 제일 최대 할인된 가격에 합류할 수 있었습니다 ㅎㅎ
또한 추천인 제도로 [추천인] 코드에 “fWHY9o”를 입력하시면 20만 원 추가 할인 혜택이 있으니 결제하실때 꼭 추천인 할인 코드도 함께 입력해주세요!
제 항해 플러스 프론트엔드 후기 글을 보고 궁금한 사항이 있으시다면 댓글이나, 벨로그 프로필 이메일, 링크드인으로 문의주세요 :)
항상 좋은 글 잘 보고 갑니다 🙌