글 작성자: bbangson
반응형

개요

1편에 이어 2편 작성하겠습니다. 

 

1편은 전체적인 프로젝트 소개와 저의 역할을 중점으로 작성한 회고.

https://bbangson.tistory.com/83

 

[기업 과제] Jaranda 팀 프로젝트 리뷰 ①

개요 2021.08.02 (월) ~ 2021.08.06(금)  원티드 프리온보딩 프론트 엔드 코스 교육에서 진행한 Jaranda 기업의 기업 과제 입니다. 총 8명에서 약 5일 동안 진행한 프로젝트입니다. 큰 규모의 프로젝트는

bbangson.tistory.com

2편과 3편은 팀원이 구현한 기능에 대한 공부와 설명.

https://bbangson.tistory.com/86

 

[기업 과제] Jaranda 팀 프로젝트 리뷰 ③

개요 3편 작성하겠습니다. 1편은 전체적인 프로젝트 소개와 저의 역할을 중점으로 작성한 회고. https://bbangson.tistory.com/83 [기업 과제] Jaranda 팀 프로젝트 리뷰 ① 개요 2021.08.02 (월) ~ 2021.08.06(금..

bbangson.tistory.com

4편은 팀 프로젝트 셋업 내용과 리팩토링 및 최종 회고.

이렇게 구성되어있습니다. 

 

 

2편은 팀원들이 구현한 기능 중에서 제가 참여하지 못했던 기능들을 몇 가지 뽑아 공부하고 설명하는 포스팅입니다. 

주로 팀원의 코드를 분석하는 시간이 될 것 같습니다. 글이 길어지는 점 양해 바라겠습니다.

 

 

2편에서는 다음 순서로 작성하겠습니다. 

 

1. 회원가입 페이지 - 주소 등록 (다음 API), 신용카드 등록, 토스트 메시지

2. 로그인 페이지 - 유저 인증 후 token 발급 -> 발급된 token을 기준으로 페이지 이동

 

 

 

 


회원가입 페이지

 

□ 주소 등록 

 

 주소 정보 등록 기능은 다음 API를 활용했습니다. 주소 등록 버튼을 누르면 다음 API를 호출한 팝업창이 나오고 주소 검색을 통하여 사용자의 주소를 등록하는 로직입니다.

 

주소 입력 폼

 

 위 사진에서 우편번호 찾기 버튼을 클릭하면 아래와 같은 팝업창이 뜹니다.

 

다음 주소 API 팝업창

 이것을 구현하는 방법을 알아보겠습니다. 

먼저 다음 API를 호출하고자 하는 페이지에 다음과 같은 코드를 넣습니다. 

<script src="//t1.daumcdn.net/mapjsapi/bundle/postcode/prod/postcode.v2.js"></script>

index.html에 적용한 모습

 우편번호 찾기 버튼에 onClick 이벤트를 통하여 다음 API 함수를 호출합니다. 

// 우편번호 찾기 onClick 
const onClickAddrBtn = () => {
    get_address(userInfo, setUserInfo);
  };

 

 다음 API 설명서를 참고하여 원하는 state에 주소 정보를 등록합니다. 

// 주소 정보 등록.
export default function get_address(info, setInfo){
    new window.daum.Postcode({
        oncomplete: function(data){
            const zcode_ = data.zonecode;
            const roadAddr_ = data.roadAddress;
            const jibunAddr_ = data.jibunAddress;
            setInfo({...info, roadAddr:roadAddr_,  zcode:zcode_, jibunAddr:jibunAddr_})
        }
    }).open();
}

 자세한 내용은 아래 사이트를 참고 바라겠습니다. 

https://postcode.map.daum.net/guide

 

Daum 우편번호 서비스

우편번호 검색과 도로명 주소 입력 기능을 너무 간단하게 적용할 수 있는 방법. Daum 우편번호 서비스를 이용해보세요. 어느 사이트에서나 무료로 제약없이 사용 가능하답니다.

postcode.map.daum.net

 

 

 

□ 신용카드 등록 (모달, 토스트 메시지)

 

 신용카드 등록은 모달을 활용했습니다. 

index.html 파일

 먼저 index.html의 파일을 보면, <div id="root"> 태그 밑에 <div id="modal-root"> , <div id=toast-root">가 포함되어 있는 것을 볼 수 있습니다. 최상위 컨테이너인 <div id="root">에서 모달 컴포넌트를 따로 구현하여 렌더링해도 되지만 굳이 위와 같이 구현한 이유는 무엇일까요?

 

 모달과 토스트 메시지는 어느 컴포넌트에서 사용되어도 사용자에게는 최우선적으로 보여줘야 합니다.

React의 경우 최상위 컨테이너인 <div id="root">에서 모든 컴포넌트가 렌더링 되지만 모달과 토스트 메시지는 root 컨테이너와 관계없이 위치를 고정시켜 사용자에게 노출하므로 root 컨테이너 트리에서 벗어나는 것이 더 깔끔합니다. 

 

 react-dom 패키지의 createPortal을 통해 root 컨테이너에서 벗어날 수 있습니다. 이 기능으로 부모 컴포넌트의 DOM 계층 구조 바깥에 있는 DOM 노드로 자식을 렌더링 할 수 있습니다. 이를 통해 기존 부모 컴포넌트에서 불필요한 스타일을 상속받을 필요도 없어지고 DOM 관리도 한결 수월해질 수 있습니다.

ReactDom.createPortal( Child, Container)

 Child에 모달 컴포넌트를 Containder에 모달을 렌더링할 컨테이너를 넣어주면 됩니다. 

첫 번째 인자는 엘리먼트, 문자열, 혹은 fragment와 같은 어떤 종류든 렌더링할 수 있는 React의 자식입니다. 

두 번째 인자는 DOM 엘리먼트입니다.

 

 다음과 같이 모달이 구현되었습니다. 

export default function Modal({ show, onClickClose, children, accountStyle }) {
  return ReactDom.createPortal(
    <>
      {show && (
        <>
          <Overlay>
            <Container>
              <Wrap accountStyle={accountStyle}>
                <Header>
                  <ModalClose onClick={onClickClose}>
                    <AiOutlineClose size={18} />
                  </ModalClose>
                </Header>
                <Contents>{children}</Contents>
              </Wrap>
            </Container>
          </Overlay>
        </>
      )}
    </>,
    document.getElementById('modal-root'),
  );
}

 

 이제 신용카드 등록은 어떤 식으로 구현되었는지 알아보겠습니다. 

SIgnUp 컴포넌트 속 모달과 토스트폼을 불러오는 모습

 SignUp 컴포넌트에서 <Modal> 컴포넌트를 불러오고 그 안에 <CreditCardForm/> 태그로 <Modal> 컴포넌트에 자식으로 넘겨주는 모습입니다. <Modal> 컴포넌트의 {children}으로 <CreditCardForm/> 컴포넌트를 받아, 모달 창 안에 신용카드 컴포넌트를 렌더링 하게 됩니다. (위의 모달 코드 참고)  

 

 

CreditCardForm.js 

더보기
import React, { useState, useEffect } from 'react';
import { style } from './CreditCardFormStyle';
import PropTypes from 'prop-types';
import {
addSeparatorBetweenNumber,getOnlyNumber,limitLength,validateExpiration,} 
from './utils/cardValidation';
import ToastForm from 'Components/ToastForm';

const INPUT_NAMES = {
  CARD_NUMBER: 'cardNumber', HOLDER_NAME: 'holderName', EXPIRED: 'expired', CVC: 'CVC',
};

export default function CreditCardForm({ closeModal, creditCard,handleCardInput, }) {
  const { cardNumber, holderName, expired, CVC } = creditCard;

  const [cardInput, setCardInput] = useState({
    cardNumber: addSeparatorBetweenNumber(cardNumber, 4, ' '),
    holderName,
    expired: addSeparatorBetweenNumber(expired, 2, '/'),
    CVC,
  });
  
  
  // 토스트 메시지 useState
  const [toast, setToast] = useState({
    status: false,
    msg: '',
  });
  // 토스트 메시지 useEffect
  useEffect(() => {
    if (toast.status) {
      const timeInterver = setTimeout(() => {
        setToast({ ...toast, status: false });
      }, 2000);
      return () => clearTimeout(timeInterver);
    }
  }, [toast]);
  
  // onChange 이벤트 관리
  const onChangeInfo = (e) => {
    switch (e.target.name) {
      case INPUT_NAMES.CARD_NUMBER:
        setCardInput({
          ...cardInput,
          cardNumber: limitLength(
            addSeparatorBetweenNumber(e.target.value, 4, ' '),
            19,
          ),
        });
        break;

      case INPUT_NAMES.HOLDER_NAME:
        setCardInput({ ...cardInput, holderName: e.target.value });
        break;

      case INPUT_NAMES.EXPIRED:
        setCardInput({
          ...cardInput,
          expired: limitLength(
            addSeparatorBetweenNumber(e.target.value, 2, '/'),
            5,
          ),
        });
        break;

      case INPUT_NAMES.CVC:
        setCardInput({
          ...cardInput,
          CVC: limitLength(getOnlyNumber(e.target.value), 3),
        });
        break;
    }
  };
  
  // 등록 버튼 유효성 검사, 정보 저장, 닫기
  const onClickSubmitBtn = () => {
    const { cardNumber, holderName, expired, CVC } = cardInput;

    if (cardNumber.length < 19) {
      setToast({
        ...toast,
        status: true,
        msg: '유효한 카드 번호를 입력해주세요.',
      });
      return;
    } else if (holderName.length < 1) {
      setToast({
        ...toast,
        status: true,
        msg: '이름을 입력해주세요.',
      });
      return;
    } else if (!(expired.length === 5 && validateExpiration(expired))) {
      setToast({
        ...toast,
        status: true,
        msg: '유효한 카드 유효기간을 입력해주세요.',
      });
      return;
    } else if (CVC.length < 3) {
      setToast({
        ...toast,
        status: true,
        msg: '유효한 CVC를 입력해주세요.',
      });
      return;
    }

    handleCardInput({
      cardNumber: cardNumber.replaceAll(' ', '-'),
      holderName,
      expired: expired.replace('/', ''),
      CVC,
    });
    closeModal();
  };

  return (
    <Container>
      <Wrap>
        <Row>
          <Title>신용카드 정보 입력</Title>
        </Row>
        <Row>
          <CardNumberInput
            name={INPUT_NAMES.CARD_NUMBER}
            value={cardInput.cardNumber}
            onChange={onChangeInfo}
          />
        </Row>
        <Row>
          <HolderNameInput
            name={INPUT_NAMES.HOLDER_NAME}
            value={cardInput.holderName}
            onChange={onChangeInfo}
          />
        </Row>
        <Row>
          <ExpiredInput
            name={INPUT_NAMES.EXPIRED}
            value={cardInput.expired}
            onChange={onChangeInfo}
          />
          <CVCInput
            name={INPUT_NAMES.CVC}
            value={cardInput.CVC}
            onChange={onChangeInfo}
          />
        </Row>
        <Row>
          <CancelButton onClick={closeModal}>취소</CancelButton>
          <CreditButton onClick={onClickSubmitBtn}>등록</CreditButton>
        </Row>
      </Wrap>
      <ToastForm show={toast.status} contents={toast.msg} />
    </Container>
  );
}

const {
  Container,Wrap,Row,Title,CardNumberInput,HolderNameInput,ExpiredInput,CVCInput,
  CancelButton,CreditButton,
} = style;

CreditCardForm.propTypes = {
  closeModal: PropTypes.func,
  creditCard: PropTypes.object,
  handleCardInput: PropTypes.func,
};

 위 컴포넌트를 통해 아래 사진과 같은 모달 창 속에 신용카드 정보 입력 컴포넌트를 렌더링 하게 됩니다. 저랑은 다르게 onChange 메소드를 switch문으로 해결한 게 인상적입니다. 등록 버튼을 누르면 유효성 검사를 하게 되고 올바르게 입력되지 않은 입력 폼에 대한 토스트 메시지를 렌더링 합니다.

 문득 든 생각이지만, 제가 1편에서 작성한 등록 버튼을 누른 후 "등록이 완료되었습니다." 토스트 메시지 기능을 위 컴포넌트에서 했으면 더 간단하고 깔끔하게 구현했을 것 같습니다. 주어진 역할에만 몰두하여 제 코드에만 갇혀서 생각하게 된 것이 오히려 더 어려운 방향으로 이끄는 길이 되었습니다. 

 

 토스트 메시지도 구현 방법은 모달과 유사합니다. 다만 차이점이 있다면 모달은 버튼에 onClick 이벤트를 통하여 show 속성을 변화시켜줬다면, 토스트 메시지는 useState Hook 함수를 사용해 show 속성을 변경해줬습니다. 그리고 useEffect Hook 함수를 사용하여 2초간 토스트 메시지를 노출시키도록 했습니다. 

토스트 메시지 useState, useEffect 사용부분

 

 

 

 

 


로그인 페이지

로그인 페이지

 

□ 유저 인증 후 token 발급 && 발급된 token을 기준으로 페이지 이동

 

 로그인 컴포넌트에 대한 로직은 간단합니다.

 

 아이디와 비밀번호를 입력받고 로그인 버튼을 누르면 유효성 검사를 시작합니다. 입력받은 정보들이 로컬 스토리지에 저장되어 있는 기존의 회원 정보와 일치하는지 검사합니다. 일치한다면 입력된 정보는 기존 회원 정보이므로, 이 회원의 정보를 저장할 token을 발급합니다. 발급된 토큰에 저장된 회원의 role을 기준으로, 이 회원이 관리자 회원인지 일반 유저인지 판단하고 그에 맞는 페이지로 이동시킵니다. 

 

 자세한 설명은 주석에 넣겠습니다. 

  // 로그인 검사
  const sendLogin = (userID, userPW) => {
    const userInfo = LOCAL_STORAGE.get('userData');
    // 기존 회원인지 검사
    let test =
      userInfo &&
      userInfo?.find(
        (data) => data.userId === userID && data.password === userPW,
      );

    // 기존 회원이라면 회원 정보를 저장할 token 발급
    if (test !== undefined) {
      LOCAL_STORAGE.set('token', {
        userId: test.userId,
        role: test.role,
      });

      return true;
    }
    return false;
  };

  // 로그인 버튼 onClick
  const onClickCheckLogin = () => {
    // 유효성 검사 
    if (
      checkId(inputIdValue) &&
      checkPassword(inputPwValue) &&
      inputIdValue !== '' &&
      inputPwValue !== ''
    ) {
      // 로그인 검사
      const validLogin = sendLogin(inputIdValue, inputPwValue);
      
      // 회원에 토큰에 따라 페이지 이동
      const tokenRole = LOCAL_STORAGE.get('token')?.role;
      if (validLogin && tokenRole === 'admin') {
        history.push(ROUTES.ADMIN);
      } else if (validLogin && tokenRole !== 'admin') {
        history.push(ROUTES.MAIN);
      }
      return;
    }
    setIsValid(true);
    setTimeout(() => {
      setIsValid(false);
    }, 6000);
  };

 

 


 2편은 여기서 마무리하도록 하겠습니다. 주니어 개발자들의 프로젝트라 미흡한 부분이 많을 거라 생각합니다. 

더 좋은 생각이나 의견이 있다면 댓글로 피드백 주시면 좋겠습니다. 

 

긴 글 읽어주셔서 감사합니다. 

반응형