[2차 프로젝트] WEHOTEL(데일리호텔 클론코딩)

dabin *.◟(ˊᗨˋ)◞.*·2021년 11월 8일
2

TeamPJT

목록 보기
2/3
post-thumbnail

프로젝트 소개

위호텔 GITHUB

FRONTEND
BACKEND

앱기반으로 되어있는 웹사이트 데일리호텔을 참고해 '위호텔' 프로젝트를 완성했다. 실제로 많은 회사에서 앱기반으로 서비스를 시작해 확장해 나가는 경우 웹사이트가 앱기반으로 되어있는 경우가 많다고 들었다. 앱개발을 배우는 것 같아 레이아웃을 만드는 것부터 신선했으며, 배포 이후 앱과 구분해 웹의 구성을 바꿀 수 있으니 확장성과 컴포넌트 재사용을 한 번 더 고려해보는 것이 중요하다고 느꼈다.

팀구성과 사용된 기술

팀구성

  • FRONTEND 4명
  • BACKEND 2명

사용된 기술

  • FRONTEND : React, Styled-Component
  • BACKEND : Node.js, Express, Prisma, MySQL, POSTMAN, JWT
  • 각종 라이브러리와 API : 카카오 로그인 API, 네이버 지도 API, React-modern-calendar, Context API

구현 기능과 역할

  • Header
    • 검색 페이지로 이동하는 링크 구현
    • 로그인 및 마이데일리 페이지로 이동하는 아이콘 기능 구현
  • SignUp/SignIn
    • 정규표현식을 활용하여 입력한 이름/생년월일/휴대폰번호의 타당성 검사 기능 구현
    • 카카오 소셜 로그인 API 연동
    • 처음 로그인시 회원가입 페이지로 이동하는 기능 구현
    • 로그인 페이지에서 로그인 성공 시, 메인 페이지로 이동하는 기능 구현
    • 로그인 페이지에서 로그인 성공 시, Header의 로그인 링크가 개인정보로 변경되는 기능 구현
    • 로그인 페이지에서 로그인 실패 시 경고창으로 로그인 실패 사실에 대한 알림 구현
  • 검색 페이지
    • 검색어 입력창 구현
    • 달력 모달 창으로 연결되어 기간을 선택할 수 있는 기능 구현
    • 검색 버튼 클릭시 검색어와 기간을 쿼리스트링을 통해 API로 전달하는 기능 구현
    • 검색어와 기간을 로컬스토리지에 저장하여 해당 검색 리스트 페이지로 이동할 수 있는 기능 구현
  • 예약 상세 페이지
    • 하나의 예약에 대한 정보를 제공하는 기능 구현

회고

1차 프로젝트 회고 당시, 하나를 만들어도 제대로 만들어 맡은 일을 해내고, 여유있게 팀원들에게 필요한 일을 할 수 있으면 좋겠다고 생각했다. 개인적으로 이러한 바람들을 모두 이룬 것 같아 만족스럽다. 해보고 싶은 기능(로그인/회원가입)을 공부하며 구현했고, 팀원들의 코드도 같이 봤으며 데이터 수집 등 공통으로 해야할 일들을 여유롭게(마음에는 여유가 없었지만 손은 여유롭게..) 할 수 있었다. 실력은 고통의 총합이다. 피곤한 만큼 성장!

'기록', '공유', '계획'의 중요성

[기록] 2차 프로젝트부터 프론트와 백이 완전히 나뉘어 작업했다. 이 과정에서 서로 같은 말을 정확히 같게 이해하는 것이 얼마나 어려운 것인지에 대해 깨달았다. 우리 팀의 회원가입 로직은 최초 로그인 이후 회원가입 페이지로 넘어가 제공 동의되지 않은 정보를 받아 회원가입 버튼을 누르게 하는 것이었다. 이 때문에 백엔드 팀원들과 미팅도 하고 만나서 몇 번 로직을 공유했으나 서로 프론트/백에서 구현할 수 없는 부분을 할 수 있다고 생각하고 있어 코드를 써내려가기에 앞서 이를 해결해야 했다. 조율을 통해 회원가입/로그인을 해결했으나 처음부터 순서와 로직을 노션 페이지에 적어가며 미팅을 했다면 풀리지 않는 문제가 있어 다시 미팅을 진행할 때 조금 더 효율적이고 수월했을 것 같다.

[공유] 프로젝트를 시작하는 시기에 먼저 API 문서가 공유되거나, 프론트 측에서 목데이터를 공유했다면 프론트엔드와 백엔드 모두 작업 시간을 단축할 수 있었을 것 같다. 백엔드에서 데이터를 보낼 때 그 안에 id라는 컬럼이 여러가지가 있을 수 있어 데이터 이름을 다르게 보내야하는데, 이런 부분을 사전에 조금은 맞출 수 있지 않나 싶다. 어떠한 이유로 프론트의 기획이 조금씩 바뀔 때도 바로 백엔드와 공유를 하는 것이 중복된 작업을 줄일 수 있다는 것을 배웠고, 조금 느리더라도 확실하게 공유하고 넘어가야 한다는 것을 깨달았다.

[계획] 어차피 우리 팀은 계획된 Task를 모두 해내고 프로젝트 마감 1-2일 전에는 API 연결만 하면 될 것이라고 생각했다. 는 경기도 오산  ༼◉_◉ ༽ 오케이.. 그동안 멘토님들이 프론트엔드랑 백엔드 순서를 맞춰서 진행하라고 한 이유를 마지막 날이 돼서야 깨닫게 되었다. 발표 전 프론트엔드 백엔드 코드 조각 조각을 끼워맞추며 흔들리는 멘탈을 간신히 부여잡았고, 이를 통해 계획과 순서의 중요성을 깨닫는 갚진 시간을 가질 수 있었다. 우리 프로젝트는 User와 관계없는 API, User, User와 관계있는 API 순으로 만들었다면 좋았을 것이다. 이또한 교훈-!

욕심 덜어내기

애자일 방법론은 괜히 있는 것이 아니다,,,

이미지 출처
하나의 기능에 모든 기능을 완벽하게 넣을 때 까지 붙잡고 있는 것은 생산적이지 않다고 느꼈다. 혼자 개발을 하는 것도 아니고 API도 기다려야 하며 기획이 변경될 수도 있고, 무엇보다 코린이는 '완벽'한 기능을 만들었다고 말할 자신감도 없다...!!! 하나의 이슈가 있다면 이 이슈를 잘게 쪼갠 후 우선 큰 그림을 모두 그리고 시작했다면 부담감이 조금은 줄지 않을까 싶다. 실제로 레이아웃과 기능을 별개로 생각하니 부담감이 훨씬 적게 느껴졌다. 물론 성취감도 컸다. 이제는 개발방법론에 대해서도 자세히 알아보고 어떻게 더 효율적으로 일할 수 있을지, 어떻게 더 좋은 팀원이 될 수 있을지 고민해봐야할 타이밍이다.

해야한다!!!!!며 열정을 불태우다 종이인형이 될 뻔한 저에게 조금은 내려놓을 수 있는 여유를 주시고 거듭되는 수많은 요청에 빠르게 응답해주신 팀원분들께 감사의 마음을 전합니다  ୧ʕʘ‿ʘʔ୨ 

코드 기록



함수형 컴포넌트에서 토글 버튼은 생각보다 훨씬 더 간단한 코드로 해결됐다. Styled-Component를 처음 사용해 스타일링을 할 때 html 태그를 사용해 네스팅했지만, 모든 태그를 분리해 styled-component로 작성하니 불필요한 div 태그를 많이 줄일 수 있었다. styled-component와 함께 사용 가능한 styled-icons 라이브러리에 폰트어썸보다 훨씬 많은 아이콘이 있어 사용시 편리했다.

function Header({ page }) {
  const [barClicked, setBarClicked] = useState(false);
  const barIconHandler = () => {
    setBarClicked(!barClicked);
  };
  return (
  	{barClicked && (
        <ShortPath 
    	  setBarClicked={setBarClicked} 
          barClicked={barClicked} 
		/>
      )}
  )
}

우리 팀은 user의 정보를 로컬스토리지에 저장하기로 해서 localStorage에 token이 있으면 로그인 버튼 대신 회원정보를 보여줄 수 있도록 했다.

const isUser = localStorage.getItem('token');
const [user, setUser] = useState({});

useEffect(() => {
  if (isUser) {
    const [userInfo] = JSON.parse(localStorage.getItem('user'));
    setUser(userInfo);
  }
}, [isUser]);

처음 useEffect...!! 이렇게 하면 되는 것 맞나 하고 떨렸다,,,

로그인/회원가입

위벅스를 만들 때 로그인/회원가입을 간단히 하고 넘어가서 너무너무 아쉬웠는데, 팀원분들께서 넘겨주셔서 로그인/회원가입을 구현해볼 수 있었다. 드디어 ヽ〳 ՞ ᗜ ՞ 〵ง 게다가 소셜 로그인이라니!?

먼저 카카오 developer페이지에서 설정을 마치고 index.html에도 설정을 해준다.

const { Kakao } = window;
function KakaoLogin({ maintainLogin }) {
  const history = useHistory();

  const kakaoLoginClickHandler = () => {
    Kakao.Auth.login({
      success: function (authObj) {
        fetch(`http://localhost:8000/user/kakao`, {
          method: 'POST',
          headers: {
            authorization: authObj.access_token,
          },
        })
          .then(res => res.json())
          .then(res => {
            if (res.isExistedUser) {
              localStorage.setItem('token', res.token);
              localStorage.setItem('user', JSON.stringify(res.userInfo));
              history.push('/');
            } else {
              history.push({
                pathname: '/signup',
                props: res.newUser,
              });
            }
          });
      },
      fail: function (err) {
        alert(JSON.stringify(err));
      },
    });
  };
  return (
    <LoginButton
      href={process.env.REACT_APP_KAKAO_AUTH_URL}
      onClick={kakaoLoginClickHandler}
    >
      <KakaoIcon />
      카카오로 시작하기
    </LoginButton>
  );
}

export default KakaoLogin;

카카오 공식 문서를 따라, 처음 로그인을 한 회원은 회원가입 페이지로 이동시키고 기존 회원이라면 로컬스토리지에 정보를 저장한다는 우리 팀의 로직을 따라 코드를 작성했다. 하지만 아직 해결하지 못한 문제가 하나 있다. 바로 로그인 유지! 로컬스토리지와 세션스토리지에 나눠 담아야 하나, 쿠키에 만료기간을 저장해서 담아야 하나(백엔드와 소통이 필요) 고민이 되어 하나를 정하지 못하고 있다. 고민 후에 보완하자.

로그인 기능 구현 이후 API를 붙이는 과정에서 백엔드 로그인 코드를 작성해볼 수 있었다. 리팩토링을 좀 더 거쳐야 할 것 같지만, 한가지 로직을 가지고 로그인 프론트-백을 모두 만져봤다는 감격스러운 점에서 코드를 담아본다..!!

const accessToken = req.headers.authorization;
    const { data } = await axios.get('https://kapi.kakao.com/v2/user/me', {
      headers: {
        Authorization: `Bearer ${accessToken}`,
      },
    });

    const isExistedUser = await userService.isExistedUser(data.kakao_account);
//kakao에서 온 정보에 담긴 이메일로 기존 회원인지 확인

    if (isExistedUser) {
      const token = await userService.createJwtToken(data.kakao_account.email);
      const userInfo = await userService.getUserInfo(data.kakao_account.email);
      return res.status(200).json({
        isExistedUser: true,
        message: '카카오 로그인 성공',
        token,
        userInfo,
      });
    } else {
      const {
        email,
        profile: { nickname },
      } = data.kakao_account;
      return res.status(200).json({
        isExistedUser: false,
        newUser: { email, nickname },
      });
    }

isExistedUser가 false면 프론트에서 회원가입 창을 띄운다. userInfo로 보낸 nickname이 NAME에 자동으로 입력되고, 나머지 정보를 받는다. 예약시 사용해야하는 서비스이기 때문에 필수로 정보를 받을 수 있도록 했다. 다음에는 번호 인증도 구현해보고 싶다.

회원 정보를 받고 정규표현식을 통과할시 안내 문구가 사라지는 기능을 구현했다. 처음에는 useState 6개를 만들어 코드의 가독성이 좋지 않았지만, 멘토님의 리뷰를 받고 객체화하여 깔끔한 코드를 작성할 수 있었다.

function Signup({ location }) {
  const history = useHistory();
  const userInfo = location.props;
  //login페이지에서 signup페이지로 이동시 location.props로 userInfo를 넘겼다
  const [inputs, setInputs] = useState({
    name: userInfo ? userInfo.nickname : '',
    phoneNumber: '',
    birthday: '',
  });

  const [inputValidated, setInputValidated] = useState({
    name: false,
    phoneNumber: false,
    birthday: false,
  });
...

유효성 검증을 할 때에는 정규표현식을 객체화해놓은 뒤 불러와 사용했으며 input에는 해당 문자만 입력될 수 있도록 제한을 뒀다. 예를 들면 아래와 같다.

const phoneNumberChangeHandler = e => {
    const { value } = e.target;
    const numberOnly = value.replace(validation.number, '');
    setInputs({
      ...inputs,
      phoneNumber: numberOnly,
    });
  };

또한 input에 변화가 있을 때마다 inputValidated에 변화를 주게 만들어 모두 true일 때만 회원가입 버튼이 정상적으로 작동하게 만들었다.

useEffect(() => {
    setInputValidated({
      ...inputValidated,
      name: validation.name.test(inputs.name),
      phoneNumber: validation.phoneNumber.test(inputs.phoneNumber),
      birthday: validation.birthday.test(inputs.birthday),
    });
  }, [inputs]);

  const signupButtonClickHandler = e => {
    e.preventDefault();
    if (Object.values(inputValidated).every(v => v)) {
      fetch('http://localhost:8000/user/signup', {
        ...

이 과정에서 array.every() 메서드를 처음 알게 되었다. 이 메서드는 배열 안의 모든 요소가 주어진 함수를 통과하는지 테스트하며, boolean 값을 반환한다.
https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Array/every

검색페이지


moment라는.. 아주 애정하는 라이브러리를 알기 전까지 시간 관련 함수를 모두 셀프로 작성했다. 덕분에 자바스크립트 실력이 부쩍 는 느낌이기는 하다.. 잘가라 나으 소중했던 함수들아.. 그리고 너무나도 간단한 moment 라이브러리 사용법은 아래와 같다. 렌더링이 될 때 최초 실행되어 검색창 기간에 default 값으로 들어간다.

useEffect(() => {
    setSearchTerm({
      startDay: moment().format('YYYY-MM-DD'),
      endDay: moment().add(1, 'days').format('YYYY-MM-DD'),
    });
  }, []);

axios와 spinner라이브러리도 사용했다.

function SearchResult({ location }) {
  const [loading, setLoading] = useState(true);
  const [data, setData] = useState([]);
  const query = qs.parse(location.search, {
    ignoreQueryPrefix: true,
  });
  const { value, startDate, endDate } = query;
  useEffect(() => {
    const encodedValue = encodeURI(value);
    const fetchData = async () => {
      try {
        setLoading(true);
        const response = await axios.get(
          `${API_ENDPOINT}/list/search?value=${encodedValue}
	   &startDate=${startDate}&endDate=${endDate}`
        );
        setData(response);
        setLoading(false);
      } catch (e) {
        console.log(e);
      }
    };
    fetchData();
  }, []);

  if (loading) return <Loading />;
  return (
    ...

import ClipLoader from 'react-spinners/ClipLoader';

function Spinner() {
  return (
    <Flex>
      <ClipLoader
        color={'#6e2c9b'}
        size={60}
      />
    </Flex>
  );
}

유용한 라이브러리가 참 많은 것 같다. 하지만 커스텀이 원하는 대로 되지 않는 경우가 있어 자세히 알아보고 선택적으로 사용해야할 것 같다.

예약상세페이지


그저 어플같이 작고 소중한 예약 상세페이지! styled-component의 props를 처음 사용해봤는데, 컴포넌트를 재사용하기에 매우 편리하다고 느꼈다. 이 기능을 사용하고 코드의 양이 많이 줄었다.

<Title titleColor={titleColor}>{title}</Title>
<Value valueColor={valueColor}>{value}</Value>

const Title = styled.p`
  padding: 15px 0;
  color: ${props => (props.titleColor ? props.titleColor : '#888')};
`;

const Value = styled.div`
  width: 70%;
  color: ${props => props.valueColor && props.valueColor};
  text-align: right;
`;
profile
모르는것투성이

1개의 댓글

comment-user-thumbnail
2021년 11월 17일

다빈님 너무너무 고생많으셨어용! 같은 팀원으로서 너무 즐겁고 많이 배우면서 재미나게 프로젝트에 임할 수 있었습니당 다빈님이 안계셨으면 우리 팀의 텐션은 아마....ㅋㅋㅋㅋㅋ 감사합니당 앞으로도 항상 파이팅팅팅!!^^

답글 달기