1차 프로젝트 기술 회고

사이트 선정 이유

우리 팀이 클론한 사이트는 바로 여기! 👉 스테이폴리오

⚡️ 우선 사이트가 너무 예뻤어요 😊
프론트엔드를 공부하기에 좋을 것 같다는 생각이 많이 들었다. 디자인 감각도 이러면서 배우는거니까!

⚡️ 기능이 다양해요 🤠
날짜, 위치, 검색, 필터, 예약 등 다양한 정보들을 보여줘야 하니 백엔드에서도 배울 것이 많다고 생각했다. 원래는 사용자에 따라 사이트도 다르게 구현하려고 했지만 (게스트/호스트 분리) 시간이 없어서 이 부분은 패스!


사용한 기술

FRONT END 🖼

HTML | CSS (Module CSS) | React JS

BACK END 📚

Node.js | Express | Prisma | MySQL | Postman


Database Diagram


나의 역할은?

프론트엔드 | Detail

🖼 레이아웃

스테이폴리오의 디테일 페이지에서 조금 수정하고 싶은 부분이 생겨 조금 다르게 구현해봤다.

⚡️ 슬라이드 위 숙소명과 하트가 잘 보이지 않음
⚡️ 슬라이드 밑에 숙소명이 중복되어 있음

🙋‍♀️ 하나로 합쳐서 디자인을 변경하면 어떨까?

⚡️ 객실 사진 보여주는 슬라이드에서 사진이 왼쪽에 살짝 보인다.

🙋‍♀️ 모두 숨기는 걸로 구현하면 어떨까?

📍 지도 API

카카오 Map API를 사용해서 화면에 지도를 출력하는 것까지 구현

✏️ 마킹해서 내가 원하는 위치를 지도에 표시하는 것까지 해보자! (리팩토링 때)

🧐 FAQ 컴포넌트

⚡️ DB에서 정보를 가져오는 것이 아니라 내용은 하드코딩했다.
⚡️ FAQ 컴포넌트에는 버튼 컴포넌트콘텐츠 컴포넌트 두 가지 컴포넌트로 이루어져 있다.
⚡️ 이 두 종류의 컴포넌트는 각각 연결되어 있으며 버튼은 state값으로 관리되고 콘텐츠 컴포넌트는 배열에 담겨있는데 state값을 index로 접근하여 해당 컴포넌트를 보여준다. 그래서 버튼을 누르면 해당 버튼과 연결된 콘텐츠 컴포넌트가 화면에 출력된다.
⚡️ 버튼을 누르면 해당 버튼만 하이라이트가 된다. 이 기능 또한 state값으로 관리하여 클릭했을 때 state값이 해당 버튼의 index값으로 설정이 된다. state값과 버튼의 인덱스값이 일치할 때 활성화된 css가 적용된 클래스명을 추가해주는 방식으로 구현했다.

백엔드 | Search & Filtering

검색어

여행지/숙소에 키워드를 입력하면 숙소명/도시/지역의 데이터에서 키워드가 들어간 데이터를 가져오는 기능

필터링


🧐 숙소 카테고리는 총 4개로 다중 선택이 가능하다. 사용자가 몇개를 선택할 지 모르니 어떻게 동적으로 관리할까 고민했다.
👉 WHERE IN 방법을 통해 4가지 모두 고를 수 있게 했고 선택된 값은 숙소 카테고리명으로 받을 수 있게 했고 선택되지 않은 값은 빈 문자열로 받았다.


const getSearchedDormitories = async (
  keyword,
  isAll,
  first,
  second,
  third,
  fourth
) => {
  keyword = "%" + keyword + "%";
  return await prisma.$queryRaw`
  SELECT * FROM
  (SELECT 
  d.id AS id,
  c.name AS category, 
  d.name AS name, 
  ci.name AS city, 
  dis.name district, 
(SELECT 
  JSON_ARRAYAGG(y.price)
  FROM dormitories x 
  JOIN rooms y ON y.dormitory_id = x.id
  WHERE x.id = d.id
  GROUP BY x.id) AS price,
(SELECT
  JSON_ARRAYAGG(y.head_count)
  FROM dormitories x
  JOIN rooms y ON y.dormitory_id = x.id
  WHERE x.id = d.id
  GROUP BY x.id) AS headCount,
(SELECT 
  JSON_ARRAYAGG(y.image_url)
  FROM dormitories x
  JOIN dormitories_images y ON y.dormitory_id = x.id
  WHERE d.id = x.id
  GROUP BY x.id) AS imageUrl
FROM dormitories d
JOIN categories c ON d.category_id = c.id
JOIN rooms r ON d.id = r.dormitory_id
JOIN cities ci ON d.city_id = ci.id
JOIN districts dis ON d.district_id = dis.id
WHERE d.name LIKE ${keyword} OR ci.name LIKE ${keyword} OR dis.name LIKE ${keyword}
GROUP BY d.id
ORDER BY d.id) AS myTable
${
  !isAll
    ? Prisma.sql`WHERE category in (${first}, ${second}, ${third}, ${fourth})`
    : Prisma.empty
};`;
};

협업 방법

약속 지키기

우리 팀은 크게 두 가지 약속이 있었다.

  1. 오전 회의저녁 회의 꼭 지키기!
    오전 회의 때는 어제까지 한 진행 상황과 오늘 할 일을 공유하고 하루 동안 작업했던 코드를 정리해서 올린 PR을 검토하고 merge했다.

  2. 오전 회의 때 한 번에 머지하기
    merge 후 develop브랜치가 새로 업데이트 되면 다시 기능별로 branch를 만들어 새롭게 진행했다. 이렇게 약속을 하니 충돌날 일도 잘 없고 git때문에 골치 아플 일이 생기지 않아 꽤 손 쉽게 협업할 수 있었다. 문제가 있더라도 오전 회의 때 함께 회의하며 해결하니 부담감도 줄었고 git이 무섭지 않았다.

노션에 모두 기록

팀 프로젝트를 하면서 정말 좋았던 부분은 사소한 부분까지 모두 노션에 기록하는 습관이었다. 회의록부터 각자 할 일, 스키마, API 형식, 발표 준비까지 모두 노션에 기록하면서 서로의 상황과 정보를 공유할 수 있었고 컨벤션을 맞추는 것은 물론 회의하다가 금방 지나간 내용까지 적으니 나중에 헤맬 일이 없어서 참 좋았다.


🥺 절대 못잊어 내 코드

FAQ 컴포넌트

FAQ Component

// FAQ 컴포넌트 

import React, { useState } from 'react';
import style from './FAQ.module.css';
import classNames from 'classnames/bind';
import FAQButton from './faqButtons/FAQButton';
import ETC from './faqContents/ETC';
import InfoForUsing from './faqContents/InfoForUsing';
import Capacity from './faqContents/Capacity';
import InfoBooking from './faqContents/InfoBooking';
import InfoAmenity from './faqContents/InfoAmenity';

const FAQ = () => {
  const cx = classNames.bind(style);
  // //infoButton 값 관리
  const [clicked, setClicked] = useState(0);

  // //infoButton 관리하는 함수
  const click = num => {
    setClicked(num);
  };
  // components in array
  const [compArray, setCompArray] = useState([
    <Capacity key={0} index={clicked} name="인원 및 금액" />,
    <InfoBooking key={1} index={clicked} name="예약 및 결제" />,
    <InfoForUsing key={2} index={clicked} name="이용 안내" />,
    <InfoAmenity key={3} index={clicked} name="부대시설 안내" />,
    <ETC key={4} index={clicked} name="기타 안내" />,
  ]);

  return (
    <div className={cx('container')}>
      <div className={cx('buttons')}>
        <p className={cx('title')}>FAQ</p>
        <FAQButton
          click={click}
          name="인원 및 금액"
          clicked={clicked}
          index={0}
        />
        <FAQButton
          click={click}
          name="예약 및 결제"
          clicked={clicked}
          index={1}
        />
        <FAQButton click={click} name="이용 안내" clicked={clicked} index={2} />
        <FAQButton
          click={click}
          name="부대시설 안내"
          clicked={clicked}
          index={3}
        />
        <FAQButton click={click} name="기타 안내" clicked={clicked} index={4} />
      </div>
      <div className={cx('contents')}>
        <p>FAQ를 통하여 예약에 관련된 더 자세한 내용들을 찾아보세요.</p>
        {compArray[clicked]}
      </div>
    </div>
  );
};

export default FAQ;

FAQButton Component

// FAQButton 컴포넌트
import React from 'react';
import style from './FAQButton.module.css';
import classNames from 'classnames/bind';

const FAQButton = ({ name, clicked, click, index }) => {
  const cx = classNames.bind(style);

  const clickButton = () => {
    click(index);
  };

  return (
    <p
      className={cx('infoBtn', clicked === index ? 'infoActive' : '')}
      onClick={clickButton}
    >
      {name}
    </p>
  );
};

export default FAQButton;

왜 기억에 남는지? 🎤

😎 처음에 이것저것 많이 고민했던게 기억이 난다.
화면 전환 없이 컴포넌트만 변경해야 하고CSS처리도 해야하고
두 개의 컴포넌트를 연결해야 하는게 생각보다 쉽지 않았기 때문이다.
그리고 뭐라고 구글링을 해야할지도 몰라서 처음엔 그냥 멍하니 쳐다보고 있었다.
어떻게 구현해야할지 감도 오지 않았던터라 처음부터 이런 저런 로직을 가져다 대고 동기들과 머리를 맞대면서 생각했던 코드라 기억에 남는다.
그리고 정말 신기하게 3일 정도 끙끙 대다가 주말 저녁에 1시간 만에
갑자기 로직이 후다닥 생각나면서 구현해내서 정말 극적인... 순간이었다.
정말 짜릿했던 경험 ⚡️

어떻게 고민하고 구현했는지? 🎤

두 가지의 조언을 받았는데
⚡️ 배열로 컴포넌트를 관리를 해라
⚡️ 컴포넌트를 나눠서 사용해라

이 두 가지의 조언을 동기들에게 받고 나서 어떻게 할지 고민했었다.
'두 가지를 한 번에 해야한다'는 압박감에 어떤 식으로 구현해야 할지 감이 오지 않았다.
그러다가 '하나씩 차근차근 하자!'라는 생각에 버튼 컴포넌트를 먼저 구현했고
그 뒤로 버튼 컴포넌트에서 사용한 state값으로 콘텐츠 컴포넌트까지 구현할 수 있었다.

이 과정을 통해 뭘 얻었는지? 🎤

🙋‍♀️ 리액트가 진짜 재밌어요!
이번 프로젝트를 통해 state와 props의 재미와 효율성을 알아버렸다.
내가 잘 사용만 하면 효율적으로 잘 코드를 쓸 수 있구나!
생각도 들고 컴포넌트를 재사용하는 것도 너무 매력적이었다.
앞으로 다른 프로젝트를 진행하면서 리액트의 매력에 빠지고 싶다.

🙋‍♀️ 삽질하고! 물어보고! 뭐든 해보자!
이번에 정말 많이 느꼈던 건 '무엇이든 남는 건 있다'라는 생각이다.
이번 기능을 구현하기 전에는 겉보기엔 간단해보이지만 나에겐 꽤 복잡한 기능이었고
어떻게 할지 감이 오지 않았기 때문에 막막했었다.
그 때 구글링하면서 이것저것 찾아보고 안되는 것도 하면서 삽질했을 때
그 때 알게 된 지식들이 다른 기능을 구현할 때 유용하게 쓸 수 있었고
다른 동기들이 막혔을 때 도움을 줄 수 있었다.

그리고 사람들은 참 다양한 생각을 가지고 있기 때문에
나와 다른 시선으로 문제에 접근할 수 있다.
그러니 내가 생각치 못한 방법이 아이디어를 제안할 수 있다.
그게 도화선이 되어 문제 해결까지 순조롭게 이어지기도 했었다.
나 혼자 끙끙 앓지 말고 다른 이에게 문제를 나눠보기도 하자!

마지막으로, 우선 시도해보고 여러가지 오류 메세지도 만나봐야 한다.
리액트를 구현하는데 재밌었던 건 내가 이것저것 시도했던 것들이
바로 바로 눈에 보이는 것이 재미있었다.
마치 게임을 하는 것 같았다.
나의 코드에 피드백을 주는 도우미와 함께 즐겁게 떠나는 여정? ^_^
여튼, 뭐든 해보면서 아닌 건 아니라는 것을 알고 그냥 어중간하게 알고 있던 지식을
짚고 넘어갈 수 있으니 뭐든지 코드 한 줄이라도 쳐봐라!

useEffect


const [detail, setDetail] = useState({
   id: 1,
   dormitoryName: '제주조랑말',
   comment: '감귤과수원의 풍경을 끌어들인 특별한 공간에서의 휴식',
   main_description:
     '제주조랑말은 감귤과수원 안에 있는 비정형적인 외관으로 프라이빗한 휴식 그리고 충실한 편안함과 동시에 특별한 제주에서의 휴식을 제공하고 싶습니다.',
   sub_description:
     '제주조랑말에서 아침에 눈을 뜨면 누운채로도 푸릇푸릇 반짝이는 귤밭을 시야에 채울 수 있으며 방문을 열면 귤 밭 사이사이에서 조잘거리는 새소리가 들립니다. 천연덕스럽게 야외복도를 넘어 자라고 있는 로즈마리의 향도 차분하게 온몸으로 스며듭니다. 느지막히 일어나서 바로 내린 커피향과 갓구운 빵냄새가 가득한 카페에서 감귤밭에 둘러쌓여 조식을 즐겨 보세요. 제주토끼는 앞만보고 달리기를 강요받는 도시사람들에게 잠시 달리기를 멈추고 몸과 마음이 쉴 수 있는 휴식을 선물하고 싶습니다.',
   city: '제주',
   district: '서귀포시',
   dormitoryImageUrl: [
     'https://images.unsplash.com/photo-1584132869994-873f9363a562?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=2070&q=80',
     'https://images.unsplash.com/photo-1551882547-ff40c63fe5fa?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=2070&q=80',
     'https://images.unsplash.com/photo-1498503182468-3b51cbb6cb24?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=2070&q=80',
   ],
   roomSpecialTitle: ['귤밭', '조식', '건축'],
   roomSpecialDes: [
     '제주조랑말은 귤밭에 둘러싸여 있어 4계절 내내 초록초록함을 느낄 수 있습니다. 특히 5월에는 숙소에 계시는동안 은은한 귤꽃향을 즐기실 수 있으며, 여름에는 풋귤을, 가을에는 주렁주렁 달려있는 감귤을, 겨울에는 밭에서 바로 딴 감귤을 맛보실 수 있습니다.',
     '제주조랑말은 화려하진 않지만 기본에 충실한 조식을 제공합니다. 로즈마리의 향과 귀를 간지럽히는 새소리를 들으며 카페에 오시면 귤밭에 둘러 쌓인 공간에서 신선한 제철 샐러드, 갓구운 빵, 직접 내리는 핸드드립 커피를 즐기실 수 있습니다.',
     '비정형적인 외관과 주변의 감귤과수원을 내부로 충분히 끌어들인 공간은 최-페레이라 건축의 설계로 탄생되었습니다. 레벨을 달리하여 더욱 프라이빗하게, 벽돌을 이용한 영롱쌓기로 제주의 바람과 빛을 더욱 변화무쌍하게 느끼실 수 있습니다.',
   ],
   recommendPlacesName: ['ICC제주국제컨벤션센터', '큰돈가', '중문색달해변'],
   recommendPlacesType: ['shop', 'food', 'nature'],
   recommendPlacesDes: [
     '국제회의전문시설이자 강연회·연회·이벤트·전시회·공연 등을 열 수 있는 복합공간',
     ' 직접 구워주는 제주 흑돼지 맛집',
     '서핑과 노을을 즐길 수 있는 해변',
   ],
 });

 useEffect(() => {
   fetch('http://localhost:8000/dormitories/2', {
     method: 'GET',
     headers: {
       'Content-Type': 'application/json',
     },
   })
     .then(res => res.json())
     .then(result => {
       setDetail(result.data[0]);
     });
 }, []);

왜 기억에 남는지? 🎤

😎 프론트에서 백엔드 api 연결까지 완료했고 잘 데이터를 받아 오는데
새로고침하면 흰 화면밖에 나오지 않았다.
이게 무슨 일인가? 😣
새벽 6시부터 계속 전전 긍긍하면서 고민하고 있다가
팀원의 도움으로 해결하고 나니 절대 잊을 수 없었다.
발표가 다가오는 압박감 속에서 99%까지 왔는데 1%가 부족한 상황!
이 때 알게된 이후로 '리액트의 생명 주기'에 대한 중요성을 많이 느꼈다.

어떻게 고민하고 구현했는지? 🎤

정말 이상하게 다른 api나 정보를 하나만 가져오는 것들은 다시 렌더링해도 잘 되길래 너무 많은 양의 정보를 가져오고 그걸 또 정제하지 않은 채로 바로 props로 넘겨서 렌더링에 문제가 생기는 건지 고민도 해봤다.
그래서 부모 컴포넌트에서 데이터를 골라서 props로 넘기기도 하고 아예 자식 컴포넌트에서 한 번 더 불러오기도 했다(이 방법은 너무 비효율적이라 마음을 얼른 접었다!)
이런 저런 삽질을 했지만 결국 다른 팀원이 알려준 건 '초기값 형태와 가져오는 데이터의 형태'였다.
그 둘의 형태가 같아야 다시 렌더링할 때 무리없이 가져올 수 있다는 것이었다.
난 그것도 모르고...😞 엄한 데서 문제 찾고 있었네
하지만 그래서 절대 잊지 않을 수 있었다.

이 과정을 통해 뭘 얻었는지? 🎤

리액트 생명 주기의 중요성을 뼈저리게 느꼈다!
2차 프로젝트 전에 생명 주기를 공부하면서
다음 프로젝트 할 때는 조금 더 렌더링을 잘 다룰 수 있도록 해보자 😎

좋은 웹페이지 즐겨찾기