[우아한테크코스 FE 5기] 레벨2 페이먼츠 미션 회고

Chex·2023년 6월 15일
0

우아한테크코스

목록 보기
11/19
post-thumbnail

페이먼츠 미션 실행화면

📝 CSS 속성 선언 순서 컨벤션 정하기

NHN 코딩 컨벤션-속성 선언 순서

출처: NHN 코딩 컨벤션

페이먼츠 미션 이전에는 css속성을 선언할 때 아무런 규칙 없이 작성했다. 점점 복잡해지는 코드에 무언가 규칙이 필요하다고 생각했고 css 속성 선언 순서 컨벤션을 찾아보았다. 레이아웃과 같이 큰 단위부터 시작하여 작은 단위로 끝마치는 선언 순서가 직관적이라고 생각했고 바로 와닿았기에 NHN 코딩 컨벤션을 따르기로 결정했다.

📝 onBlur이벤트를 처리하는 부모 요소 안에 여러 개의 input들이 있을 때, 모든 input요소가 blur됐을 때 onBlur이벤트를 처리하는 방법

페이먼츠 카드추가폼

카드번호: 1개의 fieldset(부모)요소와 4개의 input(자식)요소로 구성

const handleInputBlur = (e: React.FocusEvent<HTMLElement>) => {
	if (e.currentTarget.contains(e.relatedTarget)) return;
	
	if (!(e.target instanceof HTMLInputElement)) return;
	
	const inputs = [...cardNumber.slice(0, -1), e.target.value];
	
	if (!isValidCardNumber(inputs)) {
		setCardNumber(['', '', '', '']);
		console.error(ERROR.INVALID_CARD_NUMBER);
		allRef[0].current?.focus();
	}
};

e.currentTarget은 이벤트가 발생한 요소를 가리킨다.
e.relatedTarget은 이벤트가 발생한 후에 포커스가 이동한 요소를 가리킨다.

event.currentTarget.contains(event.relatedTarget)
이 두가지를 이용하여 이벤트가 발생한 요소와 그 자식 요소들에 포커스가 남아있는지 확인할 수 있다.

위 조건문이 true라면 이벤트가 발생한 요소나 그 자식 요소들 중에 포커스가 남아있음을,
위 조건문이 false라면 이벤트가 발생한 요소나 그 자식 요소들 중에 포커스가 남아있지 않음을 의미한다.

즉 위 상황에서는 fieldset 요소에 onBlur를 걸어두면 포커스가 카드번호 입력창 밖으로 빠져나갔을 때 카드번호 유효성 검사를 수행할 수 있다.

📝 스토리북7.0 글로벌 스타일 적용하기

// preview.tsx
import React from 'react';
import type { Preview } from '@storybook/react';
import GlobalStyle from '../src/GlobalStyle';

const customViewports = {
  Default: {
    name: 'Default',
    styles: {
      width: '400px',
      height: '700px',
    },
  },
};

const preview: Preview = {
  parameters: {
    actions: { argTypesRegex: '^on[A-Z].*' },
    layout: 'centered',
    controls: {
      matchers: {
        color: /(background|color)$/i,
        date: /Date$/,
      },
    },
    viewport: {
      viewports: { ...customViewports },
      defaultViewport: 'Default',
    },
  },
  decorators: [
    (Story) => (
      <>
        <GlobalStyle /> // 이렇게 추가해주면 된다.
        <Story />
      </>
    ),
  ],
};

export default preview;

📝 cardDataService 리팩터링하기

페이먼츠 미션 1단계에서 만들었던 cardDataService 리팩터링 과정을 정리해보고자 한다.

1단계에 적었던 PR메시지이다. 앱 외부 저장소인 로컬스토리지와 UI로직을 분리하기 위한 도메인 객체 cardDataService를 만들었고 이 객체는 도메인 데이터를 관리하는 일을 담당한다.

// cardDataService.ts

import { Card } from '../types';

const LOCAL_STORAGE_KEY = { CARD_LIST: 'cardList' };

export const cardDataService = {
  getCardList(): Card[] {
    const rawCardList = localStorage.getItem(LOCAL_STORAGE_KEY.CARD_LIST);

    return JSON.parse(rawCardList ?? '[]');
  },

  addNewCard(card: Card) {
    const storedCardList = this.getCardList();

    localStorage.setItem(LOCAL_STORAGE_KEY.CARD_LIST, JSON.stringify([card, ...storedCardList]));
  },
};

1단계 피드백

2단계 피드백

컴포넌트에서 로컬스토리지에 직접 접근하고 있지는 않지만 cardDataService를 사용하고 있었고 1,2 단계에서 위와 같은 피드백을 받았다. 그래서 고민해본 결과, UI로직과 스토리지 로직과의 의존성을 없애기 위해 cardDataServiceuseCardDataServiceuseLocalStorage라는 훅으로 분리하여 개선해보았다.

3단계 PR

// useCardDataService

import { Card } from '../types';
import { useLocalStorage } from './useLocalStorage';

const LOCAL_STORAGE_KEY = { CARD_LIST: 'cardList' };

export const useCardDataService = () => {
  const [storedCardList, setCardList] = useLocalStorage<Card[]>(LOCAL_STORAGE_KEY.CARD_LIST, []);

  const getCard = (id: string): Readonly<Card> | undefined => {
    return storedCardList.find((card) => card.id === id);
  };

  const getCardList = (): Readonly<Card[]> => storedCardList;

  const addNewCard = (card: Card) => {
    setCardList([card, ...storedCardList]);
  };

  const addAliasToCard = (id: string, alias: string) => {
    const updatedCardList = storedCardList.map((card) => {
      if (card.id === id) return { ...card, cardAlias: alias };
      return card;
    });

    setCardList(updatedCardList);
  };

  return { getCard, getCardList, addNewCard, addAliasToCard };
};
// useLocalStorage
import { useState } from 'react';

export const useLocalStorage = <T extends unknown>(
  key: string,
  initialValue: T,
): [T, (value: T) => void] => {
  const [storedValue, setStoredValue] = useState<T>(() => {
    if (typeof window === 'undefined') return initialValue;

    try {
      const item = localStorage.getItem(key);

      return item ? JSON.parse(item) : initialValue;
    } catch (error) {
      if (error instanceof Error) console.error(error.message);

      return initialValue;
    }
  });

  const setValue = (value: T) => {
    try {
      const valueToStore = value;

      if (typeof window === 'undefined') throw new Error('Cannot store Value');

      setStoredValue(valueToStore);
      localStorage.setItem(key, JSON.stringify(valueToStore));
    } catch (error) {
      if (error instanceof Error) console.error(error.message);
    }
  };

  return [storedValue, setValue];
};

3단계 피드백

👆이렇게 리팩터링을 마무리했고 이번 미션에서 관심사 분리에 대한 훅의 활용방법에 대해 배워갈 수 있었다.

📝 불필요한 State와 멀어지기

폼 입력이 완료되면 ‘다음’ 버튼이 렌더링되는 기능을 구현하고 이에 대한 코드리뷰를 받으면서 알게 된 내용을 정리해보았다.

1. 카드 등록 폼의 유효성 검사 결과를 반환하는 커스텀 훅

// src/hooks/useCardRegisterFormValidation.ts

export const useCardRegisterFormValidation = ({
  cardNumber,
  expirationDate,
  ownerName,
  securityCode,
  password,
}: Card) => {
  const isValidCardForm, setIsValidCardForm = useState(false);

  useEffect(() => {
    const isValidForm =
      isValidCardNumber(cardNumber) &&
      isValidExpirationDate(expirationDate) &&
      isValidOwnerName(ownerName) &&
      isValidSecurityCode(securityCode) &&
      isValidPassword(password);

    setIsValidCardForm(isValidForm);
  }, [cardNumber, expirationDate, ownerName, securityCode, password]);

  return isValidCardForm;

👆 카드 등록 폼에 입력된 값들에 대한 유효성 검사를 수행한 후 그 결과를 반환하는 커스텀 훅이다.

  • isValidCardForm이라는 state를 만들고 기존의 state들(cardNumber, expirationDate, ownerName, …)에 대한 유효성 검사 결과를 모두 합하여 setIsValidCardForm으로 상태값을 변경한 후 반환
  • 각 입력 값들이 변경될 때마다 실행되도록 useEffect 안에 넣어줌

2. 카드 등록 폼 컴포넌트

// src/components/CardRegisterForm.tsx

export function CardRegisterForm() {
  ...
	
  const [isValidCardForm] = useCardRegisterFormValidation({
    cardNumber,
    expirationDate,
    ownerName,
    securityCode,
    password,
  });

  ...

  return (
    <Style.Wrapper onSubmit={handleCardInfoSubmit}>
      <CardViewer cardNumber={cardNumber} expirationDate={expirationDate} ownerName={ownerName} />
      <Style.InputContainer>
        ...
      </Style.InputContainer>
      <Style.ButtonContainer>
        {isValidCardForm && <Style.NextButton>다음</Style.NextButton>}
      </Style.ButtonContainer>
    </Style.Wrapper>
  );

👆 isValidCardForm이라는 상태 값에 따라 ‘다음’ 버튼을 조건부 렌더링해주는 코드

✏️ 문제점

위 코드의 문제점은 무엇일까? 일단 리뷰어에게 받은 코드리뷰를 참고해보자.

https://user-images.githubusercontent.com/24777828/234561961-536e0442-78ea-456c-988e-10b7b127be09.png

계산된 값이란 뭘까?

Q. 개발자가 하나하나 제어하는 값 대신 계산된 값(computed value)을 고려하라는 말의 의미는 무엇인가요?

// useCardRegisterFormValidation.ts
export const useCardRegisterFormValidation = ({
 cardNumber,
 expirationDate,
 ownerName,
 securityCode,
 password,
}: Card) => {
 const [isValidCardForm, setIsValidCardForm] = useState(false);

 useEffect(() => {
  const isValidForm =
   isValidCardNumber(cardNumber) &&
   isValidExpirationDate(expirationDate) &&
   isValidOwnerName(ownerName) &&
   isValidSecurityCode(securityCode) &&
   isValidPassword(password);

  setIsValidCardForm(isValidForm);
 }, [cardNumber, expirationDate, ownerName, securityCode, password]);

 return isValidCardForm;
};

카드 등록 폼의 유효성검사를 담당하는 커스텀 훅에서 useEffect를 이용해 인풋의 입력값들이 변경되면 폼의 유효성에 대해 계산한 값(true/false) 으로 isValidCardForm의 값을 변경하고 반환하니까 ‘계산된 값’을 사용한 게 아닌가란 생각이 들었습니다.

그런데 이게 아니라면 계산된 값이라는 건 어떤 걸 의미하는 것인지 궁금합니다.

A.

리액트에서의 상태는 렌더링마다 고유의 상태를 가지며 내부에 있는 사소한 변수조차 렌더링마다 값이 달라질 수 있어요. 4기의 도리가 작성한 글 한번 확인해보시고요

제시해주신 커스텀 훅의 isValidForm <= 이게 바로 계산된 값입니다
isValidCardForm  <= 이건 계산된 값이 아니에요

첵스의 프로필에 나오는 강아지에게

  • 9시, 6시에 밥 먹어라고 말만하고 알아서 먹도록 둔다 (계산된 값)
  • 9시, 6시가 되면 밥 먹으라고 직접 말한다 (setState())

✨ 피드백을 다시 정리해보면?

  1. 리액트에서 상태는 렌더링마다 고유의 상태를 가지며 내부에 있는 사소한 변수 조차 렌더링마다 값이 달라질 수 있으므로
  2. isValidCardForm이라는 state를 사용하는 것은 사이드 이펙트 발생 + 관리 포인트가 늘게 되는 문제가 있다.
  3. 그러니까 불필요한 state(isValidCardForm)를 만들지 말고, 기존 state 변수들(cardNumber 등)을 이용해 카드 등록 폼의 유효성에 대해 계산할 수 있으니 계산된 값(isValidForm)을 이용해라!

✨ 리팩터링 후

export const useCardRegisterForm = () => {
  ...

  const isValidCardForm =
    isValidCardNumber(cardNumber) &&
    isValidExpirationDate(expirationDate) &&
    isValidOwnerName(ownerName) &&
    isValidSecurityCode(securityCode) &&
    isValidPassword(password);

  return {
    ...
    isValidCardForm,
  };
};

Be the best version of you!

profile
Fake It till you make It!

0개의 댓글

관련 채용 정보