Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MISSON2 / 이규민 #10

Open
wants to merge 27 commits into
base: main
Choose a base branch
from

Conversation

Klomachenko
Copy link
Collaborator

@Klomachenko Klomachenko commented Mar 25, 2024

1. 구현 모습

  • [mission] 2주차 미션입니다! #8
  • 화면의 경우 쿠폰을 적립하거나, 발급할 때 보통 모바일 화면을 사용하기에, 가장 작은 아이폰 SE 기준으로 구현해 보았습니다!
    화면 기록 2024-03-25 오후 9 14 32 (4)

2. 해결 과정

배포 링크

기능 요구사항

  • 스탬프 발급하기
  • 스탬프 적립하기
  • 스탬프 10장시 쿠폰 발급하기
    • 스탬프 10장 되자마자 쿠폰 자동 발급
    • 자동 발급되며 스탬프 초기화
  • 쿠폰 적립하기
  • 쿠폰 사용하기
    • 쿠폰 사용시 사용한 쿠폰 사라지기

TEST

  • 주문하기 버튼 클릭시 스탬프 발급되는지
  • 스탬프 10장 적립시 쿠폰 발급되는지
  • 쿠폰이 발급되며 스탬프가 초기화 되는지
  • 쿠폰 사용시 쿠폰이 사라지는지

플로우

  • 사용자가 적립하기 버튼 Click
  • 스탬프가 적립하기 버튼을 Click할때마다 발급됨
  • 스탬프가 10개가 됨
    • 쿠폰 발급 완료 (toast-message)
    • 스탬프 10개 삭제
  • 쿠폰 사용하기
    • 쿠폰 사용 완료 (toast-message)
    • 쿠폰 사용시 쿠폰이 사라짐

파일 구조

  • 파일 구조 보기

    src
     ┣ assets
     ┃ ┣ images
     ┃ ┃  ┗ ToastMessage
     ┃ ┗ ProfileImage.jpg
     ┣ components
     ┃ ┣ atom
     ┃ ┃	┣ DefaultButton
     ┃ ┃	┣ TabButton
     ┃ ┃	┣ Stamp
     ┃ ┃    ┗ Coupon
     ┃ ┃    ┗ ToastMessage
     ┃ ┗ block
     ┃ ┃	┣ ProfileModal
     ┃ ┃	┣ StampModal
     ┃ ┃    ┗ CouponModal
     ┣ Page
     ┃ ┗ MainPage
     ┣ constants
     ┃ ┗ UserData.js
     ┣ css
     ┃ ┗ Card.css
     ┣ styles
     ┃ ┗ theme.js
     ┣ App.css
     ┣ App.jsx
     ┣ index.css
     ┗ main.jsx
    

컴포넌트 분리의 기준

저번 1주차 미션 중 동완님의 미션에 기표님의 리뷰를 참고하였습니다.

  • 기존 Atomic 디자인 패턴의 경우
    • Atoms
    • Molecules
    • Organisms
    • Templates
    • Pages
  • 하지만, 현재 프로젝트에선 저렇게 많은 단계가 필요하지 않다 여겨져 기준을 세워 분리하였습니다.
    • 재활용 되는가?
    • 상위 단계에 하위 단계가 포함되는가?
    • 내부에서 따로 이벤트에 따라 렌더링이 발생하지 않는 요소 : atom
    • 이벤트에 따라 렌더링이 발생하는 탭 : block
    • 전체를 관리하고, 각 block의 연결된 상태를 관리하는 곳 : Page

결국, 아토믹 디자인 패턴도, 많은 사람들이 ‘쓰다 보니, 이렇게 하면 좋더라’라고 생각하였고,

이번 미션에 알맞게 컴포넌트를 세 단계로 분리하였습니다

폴더 구조(img)

Public vs assets

  • Public 폴더
    • 정적 파일을 저장하는 곳
    • 리액트 앱에서 공용으로 사용되는 이미지, 외부에서 가져온 이미지 등을 저장하는데 사용
    • 빌드 시에 웹펙과 같은 번들러에 의해 처리되지 않고, 그대로 유지
  • assets 폴더
    • 앱 소스 코드와 관련된 이미지를 저장하는 곳
    • 앱의 구성 요소에서 ‘동적’으로 사용됨
      • 사용자가 업로드한 이미지, 컴포넌트에서 사용하는 배경 이미지 등
    • 컴파일 시에 번들러에 의해 처리됨

현재 쿠폰이나, 스탬프 등의 이미지는, ‘디자인’을 나타내는 이미지이며,

사용자에게 쿠폰이나 스탬프의 시각적인 정보를 전달합니다.

또한, 해당 이미지를 사용하는 컴포넌트가 사용자에 의해 ‘동적’으로 사용되므로, assets 폴더에 넣는 것으로 결정하였습니다.

스타일

  • 스타일의 경우 module.css를 이용해 css 스타일이 겹치는 것을 방지하였습니다.

아쉬운 점

  • module.css를 사용하니, 실제 console에서 보이는 className이 제가 실제 작성한 이름과 매우 달랐고, 그로 인해 toastMessage에 애니메이션을 간단하게 넣는 것을 하지 못했습니다.

해결 방법

  • toastMessage를 직접 구현하려 했는데, react-toastify 라는 라이브러리를 이용해 보면 매우 편할 것 같습니다.

상태 관리

현재 상단에 작성해놓은 플로우에 따르면 ‘쿠폰’과 ‘스탬프’가 유기적으로 상호작용 하는 것을 볼 수 있습니다.

하지만, 컴포넌트 분리를 생각해 보았을 때, 쿠폰과 스탬프는 현재 부모, 자식 요소가 아닙니다.

위와 같은 상황에서 2가지 선택지가 있다고 생각합니다.

  1. 상태관리 라이브러리 (zustand 등)
    • 따로 store로 분리해서 상태를 관리
  2. 상태를 부모 컴포넌트로 옮기고 관리하는 방법
    • 상태가 중복되면 상단으로 올려서 관리
  • 이번 미션에서는 2번을 사용해볼 예정입니다. 사용 이유는 다음과 같습니다.
    • 상태관리 라이브러리를 너무 남발하여 너무나 많은 store가 생겼던 경험이 있습니다.(디버깅 어려움)
    • 아직 state와 props를 통해 데이터를 넘기는 것이 아직 미숙합니다.
    • state, props가 어떻게 넘겨지는지 흐름을 파악한다.
    • 상태관리 라이브러리는 이미 사용 해보았으므로, state, props를 명확하게 다뤄보고 비교해본다.

상태 끌어올리기

위에서 말했듯이, 현재 ‘CouponModal’과 ‘StampModal’이 서로 반드시 데이터를 공유해야 합니다.

  • Stamp 갯수에 따른 Coupon 증가

하지만, 현재 CouponModal과 StampModal 간에는 props 전달 방식으로 데이터 공유가 불가능합니다.

이러한 경우 해결 방법은

  • 그 값을 필요로 하는 컴포넌트 간의 가장 가까운 공통 조상으로 state를 끌어올림으로써 이뤄낼 수 있다.
    이렇게 하는 방법을 “state 끌어올리기”라고 부릅니다.
  • 그 값을 필요로 하는 컴포넌트
    • StampModal
    • CouponModal
  • 가장 가까운 공통 조상
    • MainPage
  1. 메인페이지에서 StampNum을 props로 넘겨주고, handleSaveStamp 함수를 넘겨줍니다.
    • StampNum : 스탬프의 갯수를 useState로 저장합니다.
    • handleSaveStamp : 스탬프의 갯수가 10개일 경우 쿠폰 갯수를 관리합니다.
      • handleSaveStamp는 buttonClick을 통해 saveStamp 형식으로 StampModal에서 실행됩니다.
        (너무 복잡하게 실행되는 느낌입니다. 그냥 handleSaveStamp 자체를 넘기는 방법으로 리팩터링 필요)
  2. StampModal에서 넘겨받은 함수를 실행해 데이터를 변경합니다. 이 때 MainPage의 데이터가 변경됩니다.
  3. 메인페이지에서 couponNum을 props로 넘겨주고 handleUseCoupon 함수를 넘겨줍니다.
    • couponNum : 쿠폰의 갯수를 useState로 저장합니다
    • handleUseCoupon : 쿠폰이 사용되는 경우 쿠폰 갯수를 관리합니다.
      • handleUseCoupon는 buttonClick을 통해 useCoupon 형식으로 CouponModal에서 실행됩니다.
        (너무 복잡하게 실행되는 느낌입니다. 그냥 handleUseCoupon 자체를 넘기는 방법으로 리팩터링 필요)

상태 관련 질문점

  • 어디까지 공통의 부모 컴포넌트에서 관리해야 하는가?

    • 예시로 StampModal의 경우
    • 스탬프의 갯수(stampNum), 스탬프의 handleSaveStamp와 같은 로직 모두 다 MainPage에서 관리하고 있음
    • 과연 이게 왜 좋은가?
      • 재사용성 : 로직이 부모 컴포넌트에 있을 경우 비슷한 로직을 가진 다른 컴포넌트에 재사용이 편합니다.
      • 단일 책임 원칙 : 각 컴포넌트는 하나의 책임을 가져야 합니다.
        • 다만, MainPage의 경우 하나의 책임이 아닌 너무 많은 책임을 가지고 있는 것 같습니다.
    • 단점은 무엇이 있을까?
      • 디버깅 : 에러가 발생했을 때 MainPage에 관련된 모달이 10~20개정도 되었을 경우, 디버깅 하기 힘들고, 컴포넌트 관리가 힘들지 않을까?
        • 이럴 땐 hooks로 분리하면 되지 않을까 싶습니다!

3. To 리뷰어에게

  • ‘저만의 기준’을 세워 컴포넌트를 분리하였습니다. 혹시 적절한 분리였는지 궁금합니다!
  • 이번 미션에서 ‘매우 복잡한 로직’은 없었던 것 같습니다. 하지만 아직까지 비즈니스 로직, ui 관련 로직이 명확하게 머릿속에서 구분되지 못하는 느낌입니다.
    • 비즈니스 로직을 따로 분리하여 hooks 로 분리하려 하는데, 이번 미션에서는 ‘handleStamp’, ‘handleUseCoupon’ 정도가 비즈니스 로직이라고 생각합니다.
    • 혹시 멘토님께서는 어떻게 생각하시는지 궁금합니다! 또한 비즈니스, ui 로직을 분리하는 기준이 있으신지도 궁금합니다
  • TEST라고 적힌 항목을 보시면, 해당 항목들을 테스트코드를 작성하여 진행해보고 싶었습니다. 다만, 전에 진행해본 테스트코드의 경우 우테코 미션과 같이 이미 주어진 환경에서 jest만 사용하여 테스트를 진행하였는데, 이렇게 로직과 ui가 한 컴포넌트에 묶인 경우엔 어떤 방식으로 테스트를 진행하는지 궁금합니다!
    또한, TEST 목록이 적절한지도 궁금합니다.
    • 또한 테스트코드 작성을 위해 hooks로 분리해야 하는지, utils로 분리해야 하는지 해당 부분을 이제 시작해보려 하는데, 리액트 테스트코드 작성은 어떤 방식으로 하시는지 궁금합니다!

아래는 리팩터링 할 목록입니다. 혹시 올바르게 찾았는지, 더 할 게 있는지 확인해주시면 매우 감사하겠습니다.

  • 스탬프가 10이 아니라 11번째 클릭때 발급된다는 메시지

    • if 문 내부의 조건을 10에서 9로 변경하였습니다.
    • 혹시 더 나은 방법이 있는지 궁금합니다!
      분명 state를 변경하도록 했는데 클릭시에 1이 아니라 0이 콘솔에 출력되는 이유가 궁금합니다.
     const handleSaveStamp = () => {
        setStampNum(stampNum + 1);
        console.log(stampNum);
        if (stampNum === 9) {
          setToast(true);
          setMessage('쿠폰이 발급되었습니다!');
          setCouponNum(couponNum + 1);
          setStampNum(0);
        }
      };
    • 만약, 더 나은 방법이 없다면, 10개가 되는 경우 ‘적립하기’에서 ‘쿠폰 발급받기’로 버튼 이름을 바꿀 생각이었습니다. 하지만 너무 1차원적인 회피성 방법이라 생각됩니다.
  • 쿠폰이 순차적으로 삭제만 될 뿐

    • 만약 쿠폰의 종류가 달라 특정 쿠폰을 사용하게 된다면…?

    • 특정 쿠폰을 클릭해서 삭제해야 하는 경우엔 리스트에 담는 형식으로 구현해야 할 것 같습니다!

    • 리스트에 담아서, 클릭한 쿠폰의 인덱스를 삭제하는 방식!

      (현재는 숫자로 관리해서 숫자만큼 반복하여 stamp, coupon을 보여주고 있습니다)

      (숫자로 관리한 이유는 뭔가 테스트할 때 숫자가 조금 더 직관적일 것이라 생각했기 때문입니다.)

귀중한 시간 내주셔서 정말 감사합니다!


geongyu09 and others added 27 commits March 12, 2024 22:41
Description: fontSize, backgroundColor, color, borderColor를 미리
설정합니다
Description: 카드 컴포넌트를 생성합니다
Description: theme에서 중복되는 색상들을 삭제하고 하나로 통일하였습니다
Description: 가장 기본적인 형태의 Card Component를
구현하였습니다(최소기능)
Description: module.css로 변경함에 따라 내부 className을 수정하였습니다
Description: 스탬프 구현을 위해 image도 assets 폴더에 추가하였습니다
Description: 쿠폰 구현을 위한 Coupon 이미지 assets에 추가
Description: Modal 별로 DefaultButton의 buttonText를 props를 통해
전달합니다
Description: 현재 적립하기의 경우 쿠폰 페이지 클릭시 stamp가 사라지고,
상태관리 또한 부적절하게 발생하고 있으므로, 리팩터링이 필요
Description: 기존 stampModal에서 stamp 적립시, 탭이 바뀌면 stamp들이
사라지는 문제를 해결
Descripiton: state 끌어올리기를 통해 스탬프를 적립하고, 스탬프 적립에
따라 쿠폰이 발급되며, 쿠폰이 사용되는 로직을 구현하였습니다
Description: stampNum의 조건을 수정해 스탬프가 10장이 되자마자 쿠폰을
발급하도록 하였습니다
@Klomachenko Klomachenko added the mission 미션 입니다! label Mar 25, 2024
@Klomachenko Klomachenko self-assigned this Mar 25, 2024
@vvvpiano
Copy link
Member

vvvpiano commented Apr 1, 2024

‘저만의 기준’을 세워 컴포넌트를 분리하였습니다. 혹시 적절한 분리였는지 궁금합니다!

개인적으로 나름의 명확한 기준을 세워 컴포넌트를 분리했다는 점에서 이 부분에 대한 이견은 없습니다!

개인적으로 네이밍에 대해 언급하고 싶은 2가지는 아래와 같은데요

  1. 폴더 네이밍에 대해 PR에 설명해두긴 했지만, 소스코드를 보았을 때 block이라는 이름이 바로 감이 오진 않았습니다. block 내부에 ~~Modal.jsx 파일들이 위치했으므로 폴더 이름이 ‘modal’이 되어도 자연스러웠을듯 싶습니다
  2. Modal은 일반적으로 메인 윈도우 위에 사용자 상호작용을 위한 별도의 영역을 띄우는 UI를 지칭합니다. 구현해주신 형태는 전형적인 tab UI의 형태를 띄므로 Modal이라는 postfix는 부적절해보입니다. Panel 정도의 postfix를 사용해볼수 있을것 같습니다

@vvvpiano
Copy link
Member

vvvpiano commented Apr 1, 2024

이번 미션에서 ‘매우 복잡한 로직’은 없었던 것 같습니다. 하지만 아직까지 비즈니스 로직, ui 관련 로직이 명확하게 머릿속에서 구분되지 못하는 느낌입니다.
비즈니스 로직을 따로 분리하여 hooks 로 분리하려 하는데, 이번 미션에서는 ‘handleStamp’, ‘handleUseCoupon’ 정도가 비즈니스 로직이라고 생각합니다.
혹시 멘토님께서는 어떻게 생각하시는지 궁금합니다! 또한 비즈니스, ui 로직을 분리하는 기준이 있으신지도 궁금합니다

저도 아직까지 여기에 대한 명확한 기준은 가지고있지 않은데요, 사실 정답이 없는게 당연하다고 생각합니다.
둘을 분리하려고 하다가 필요이상으로 파일이 많아지기도 하니까요.
다만 제가 코드 작성하면서 한 번씩 생각해 보는 기준은

  1. ui 컴포넌트: 스토리북 작성하기 좋으려면 어떻게 짜야할지 고민
  2. 비즈니스 로직: 테스트하기 편리하려면 어떻게 짜야할지 고민, 줄글로 표현된 기획서의 요구사항을 비즈니스 로직쪽 코드를 보고 이해하기 쉽게

이정도 되는 것 같아요.
1을 생각하면서 짜다보면 UI 컴포넌트는 전달받은 prop값에 따라 알맞은 형태로만 보여주는 것이 편할 것이고, 전달받은 값에 따라 수행해야할 복잡한 동작들이 있다면 onClick, onSuccess등의 콜백함수로 넘겨받아 수행하도록 작성하면 효과적입니다. 요구사항이 단순하긴 하지만 규민님도 이와 유사한 방법으로 잘 작성해주신것 같습니다.
2를 지금 상황에 적용해보면 요구사항은 대략

  • 적립하기 버튼을 누르면 스탬프가 쌓인다
  • 스탬프가 10개가 되면 쿠폰이 1개 발급된다
  • 쿠폰 사용하기를 누르면 쿠폰이 사용된다
    이정도인것 같은데용, 말씀하신대로 규민님이 작성하신 코드에서 handle~ 코드들에 해당하는 로직이 이에 해당하므로 적절하게 잘 작성하신것 같습니다.

질문 주신 부분 중에서 hook과 util 분리를 고민중이신것 같았는데요,
위에서 언급한 비즈니스로직들은 hook으로 분리하면 좋을것 같습니다.
util은 주로 내가 코드를 작성하고있는 서비스의 도메인지식과 큰 관련 없는, 자잘하고 반복되는 작업들, 상태와 관련없이 같은 input에는 항상 같은 output이 나오는 작업들을 분리해낼 때 적절한 것 같아요
(e.g. url에서 path, query param등을 추출하는 함수, Date 객체를 일정한 형식의 문자열로 포맷팅해 리턴하는 함수)

hook을 예시로 대강 작성해보자면 아래처럼 할 수 있을것 같습니다.(참고만 해주세요!)

import { useState } from "react";
import { useToast } from './useToast'; // 토스트 노출 기능을 Hook으로 분리했다고 가정

const useCoupon = () => {
  const [couponNum, setCouponNum] = useState(0);
  const [showToast] = useToast();

  const publishCoupon = (onSuccess) => {
    setCouponNum((prev) => prev + 1);

// 이렇게 작성하거나
    if (onSuccess && typeof onSuccess === 'function') {    
      // 쿠폰이 발급되었습니다 토스트 메시지를 직접 띄우든, 문자열만 리턴하든...
      onSuccess();
    }
// 이렇게 성공시 항상 토스트메시지를 보여주도록 할 수 있다
    showToast('쿠폰 발급에 성공하였습니다!');
  }
  
  const consumeCoupon = () => setCouponNum((prev) => {
    if (prev <= 0) {
      showToast('사용가능한 쿠폰이 없습니다!');
      return prev;
    }
    return prev - 1;
  });

  return {
    couponNum,
    publishCoupon,
    consumeCoupon,
  }
}

export default useCoupon;

이렇게 작성했을때의 장점입니다.

  1. 서비스가 커졌다고 가정했을 때, 여러 다른 종류의 페이지에서 쿠폰을 받거나 사용하게 될 수 있습니다. 하나의 서비스에서 쿠폰 관련된 동작을 실행할 때 hook의 리턴값들을 재사용한다면 쿠폰 관련된 로직(e.g. 현재 쿠폰개수를 받아오는 api, 쿠폰 발급 성공/실패시 동작)을 재사용하여 일관된 사용경험을 제공할 수 있습니다.
    2.couponNum 상태에 디펜던시가 강한 함수들을 한 곳에 모아두므로 작성과 사용시 모두 편리합니다.

여유가 되신다면 hook을 사용해서 리팩토링해보시면 좋을것같아요!
그래서 MainPage에서는 최대한 hook을 가져다쓰는 방향으로,,

@vvvpiano
Copy link
Member

vvvpiano commented Apr 1, 2024

TEST라고 적힌 항목을 보시면, 해당 항목들을 테스트코드를 작성하여 진행해보고 싶었습니다. 다만, 전에 진행해본 테스트코드의 경우 우테코 미션과 같이 이미 주어진 환경에서 jest만 사용하여 테스트를 진행하였는데, 이렇게 로직과 ui가 한 컴포넌트에 묶인 경우엔 어떤 방식으로 테스트를 진행하는지 궁금합니다!

jest로도 로직과 ui를 한번에 테스트하도록 할 수 있습니다.
아래는 이번 미션과 전혀 관계없는 DatePicker 컴포넌트를 jest로 테스트하는 코드의 예시입니다.

  describe('show props', () => {
    test('show가 true일 때, 달력이 노출된다.', () => {
      // given
      const datepicker = <Datepicker show={true} />;

      // when
      const { container } = render(datepicker);

      // then
      const calendar = container.querySelector(`.${DATEPICKER_CLASSNAMES.LAYER}`);
      expect(calendar).toHaveClass(DATEPICKER_CLASSNAMES.LAYER_ACTIVE); // 캘린더가 특정 스타일로 렌더링되었는지를 className 포함 여부를 통해 확인할 수 있다.
    });

조금 더 긴 동작을 테스트해보고싶다면 cypress와 같은 툴을 사용해서 스토리북에서 잘 동작하는지 e2e테스트를 작성해볼 수도 있습니다.
아래는 회사 테스트코드 파쿠리해왔는데요,,비밀이에요,,
그래서 좀 옛날방식일수있는데 대충 이런식으로 실제 렌더링된 DOM element를 확인해보면서 테스트할 수도 있습니다.

describe('DATETIME/Datepicker', () => {
  before(() => {
    cy.visitStorybook(); // cy는 cypress
  });

  it('Datepicker-Date 타입 > 날짜를 선택했을 때 해당 날짜가 선택되는지 확인합니다.', () => {
    cy.loadStory('datetime datepicker', 'date 타입')
      .find('.tbl-calendar__day')
      .should('have.length', 35)
      .each(($el) => {
        const [day] = $el;
        const clickable = ![...day.classList].some(
          (className) => className === 'tbl-calendar__day--empty' || className === 'tbl-calendar__disabled'
        );
        if (clickable) {
          const date = parseInt(day.outerText) < 10 ? `0${day.outerText}` : day.outerText;
          cy.wrap($el)
            .click()
            .get('.input-tf')
            // 클릭가능한 날짜를 선택했을 때, 예상한 날짜 값이 input에 들어가는지 확인합니다.
            .should('have.value', '2019.11.' + date)
            .get('.btn-calendar')
            // 확인 후에는 캘린더를 확장합니다.
            .click();
        }
      });
  });
});

@vvvpiano
Copy link
Member

vvvpiano commented Apr 1, 2024

분명 state를 변경하도록 했는데 클릭시에 1이 아니라 0이 콘솔에 출력되는 이유가 궁금합니다.

리액트 공식문서(https://react.dev/learn/state-as-a-snapshot#rendering-takes-a-snapshot-in-time)를 참고하시면 이해가 되실것같아요!

“Rendering” means that React is calling your component, which is a function. The JSX you return from that function is like a snapshot of the UI in time. Its props, event handlers, and local variables were all calculated using its state at the time of the render.

Setting state only changes it for the next render. During the first render, number was 0. This is why, in that render’s onClick handler, the value of number is still 0 even after setNumber(number + 1) was called:

즉 리액트는 스냅샷처럼 렌더당시의 state값을 가지고 로직을 수행합니다.
렌더링 사이에는 state 값이 절대 바뀌지 않고, 바뀐 state는 그 다음 렌더링에 사용됩니다.
(리액트에서는 이벤트핸들러의 수행이 끝난 뒤에 다음 렌더링을 수행합니다)

    setStampNum(stampNum + 1);
    console.log(stampNum);

처음 화면이 렌더링 될 때 stampNum이 0이었다면 그 다음 렌더까지는 stampNum 값으로 계속 0을 사용하기 때문에 말씀하신 현상이 발생합니다.

혹시 더 나은 방법이 있는지 궁금합니다!
증가된 stampNum을 잠시 담아둘 변수(e.g. nextStampNum)를 만들어서 개수확인에 사용하면 됩니다.

그리고 추가적으로 publishCoupon, publishStamp, resetStamp처럼 동작에 이름을 붙이면

  • 스탬프 버튼을 누를때마다 스탬프를 발급한다
  • 스탬프가 max값(10개)에 도달하면 쿠폰을 하나 발급하고 스탬프를 리셋한다

요구사항 또는 기획을 코드로 그대로 옮길 수 있게 됩니다.
추가적으로 이 함수들을 useCoupon, useStamp등과 같은 hook에서 정의하여 리턴하면 더 깔끔할듯합니다.
아래 예시는 실행시켜보진 않았지만 대충 이런식으로 작성하면 어떨지 예시로 적어봤어요.

  const publishCoupon = () => {
    setCouponNum(prev => prev+ 1);
  }
  const publishStamp = () => {
    setStampNum(prev => prev + 1);
  }
  const resetStamp = () => {
    setStampNum(0);
  }

  const handleSaveStamp = () => {
    const MAX_STAMP_NUM = 10;
    
    const nextStampNum = stampNum + 1;
  
    if (nextStampNum >= MAX_STAMP_NUM) {
      publishCoupon();
      setToast(true);
      setMessage('쿠폰이 발급되었습니다!');
      resetStamp();
      return;
    }

    publishStamp();
  };

import Coupon from '../atom/Coupon';

const CouponModal = ({ buttonClick, couponNum }) => {
const useCoupon = () => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

추후에 hook으로 분리할거라면 이렇게 'use'가 붙는 네이밍은 훅과 혼동할 여지가 있을것같습니다. applyCoupon 등 다른 동사를 활용해보면 어떨까요?


const renderCoupon = () => {
const coupon = [];
for (let i = 0; i < couponNum; i++) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

지금은 별도로 복잡한 요구사항이 없어 coupon, stamp 모두 카운팅하는 방식으로 작성했지만
스탬프별로, 쿠폰별로 다른 이름, 타이틀, 발급 날짜 등..을 갖게 되어 구별할 필요가 있어진다면 id필드를 가진 객체의 배열로 관리하는게 더 좋아보여요!

e.g.)

const coupon = [
  {
    id: 0,
    title: '생일 할인 쿠폰',
    publishDate: '2024-04-01',
    expireDate: '2024-04-30',
  },
  {
    id: 1,
    title: '스탬프 10장 할인',
    publishDate: '2024-03-29',
    expireDate: '2024-12-31',
  }
];

github: 'https://github.com/Klomachenko',
youtube: 'https://www.youtube.com/channel/UCpxKdR3scZUtwjz_E-uIDyw',
};
const linkClick = (link) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

함수명은 습관적으로 동사로 시작하도록 지으면 좋습니당 e.g. clickLink

Comment on lines +12 to +14
for (let i = 0; i < stampNum; i++) {
stamp.push(<Stamp />);
}
Copy link
Member

@vvvpiano vvvpiano Apr 1, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

배열의 내장메소드 map을 이용하면 다음과 같이 수정할 수 있습니다

const renderStamp = () => new Array(stampNum).map((i) => <Stamp key={i}/>);

for문은 index의 증감을 개발자가 임의로 컨트롤할 수 있기 때문에, 로직이 복잡해질 경우 잠재적 오류를 발생시킬 여지가 있습니다. 특정한 상황(e.g. break문을 사용해야 하는 경우)을 제외하고는 배열의 순회를 프로그래밍언어에게 맡기도록 하는 map, forEach 등을 사용하는 것을 권장합니다.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

그리고 제가 알기로는 renderStamp처럼 함수로 정의하면 StampModal이 리렌더될때마다 renderStamp함수도 재실행되어 성능에 악영향을 줄 것 같은데요, 다음과 같이 변수로 값을 계산하여 저장해두면 리렌더시 재계산을 하지 않아 이렇게 작성하면 더 좋을것같습니다.

  const stamps = new Array(stampNum).map((i) => <Stamp key={i}/>);

  return (
    <div>
      <div className={styles.container}>
        <div className={styles.buttonBox}>
          <DefaultButton buttonText={'적립하기'} onClick={saveStamp} />
        </div>
        <div className={styles.contentBox}>{stamps}</div>
      </div>
    </div>
  );

const handleSaveStamp = () => {
setStampNum(stampNum + 1);
console.log(stampNum);
if (stampNum === 9) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

9와 같은 매직넘버는 반드시 변수, 상수화시키기를 권장합니다~

@vvvpiano
Copy link
Member

vvvpiano commented Apr 1, 2024

안녕하세요 규민님, 코드리뷰도 늦은 주제에 말이 많아서 놀라셨죠?
PR description을 상세하게 적어주셔서 어떤 고민들을 하면서 코드를 짜셨는지 잘 이해할 수 있었습니다.
모든 질문에 답변을 하지도 못했고, 저도 많이 미숙한 응애개발자라서 허접한 답이었을수 있지만;;
저의 리소스가 허락하는 범위 내에서 열심히 고민해보고 답을 달아봤습니다.
물론 그러고 계시겠지만 다른분들 PR 리뷰들도 참고하셔서 더 풍부한 답을 얻어가시길 바랍니다..!
리뷰 늦어서 죄송하고 고생하셨습니다!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
mission 미션 입니다!
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants