[project] 돌하룻밤 프로젝트 회고록

Minju Kim·2022년 5월 8일
3

Project

목록 보기
5/5
post-thumbnail

2차 프로젝트를 마치고 기업협업에 필요한 typescript, react-native를 익히느라 바빴던 2주였다. 월요일이 시작되기 전 필요한 정리들을 하고 그 중에 하나로 드디어 2차 프로젝트 회고록을 작성해 보려고 한다.

🍊돌하룻밤🗿 프로젝트

1. 개발 기간 및 인원

  • 개발 기간 : 2022.04.11 - 2022.04.21 (11일)
  • 개발 인원 : 6명 ➡️ Front 4명(김민주, 김보윤, 김동욱, 윤서영) / Back 2명(한상안(PM), 김수훈)

2. 사용한 기술 스택

  • Front-End : React.js, React Router, Styled-Components, Material UI, Kakao 로그인 및 지도 API, React Slick, Date-Picker
  • Back-End : Python, Django, AWS(EC2, S3, RDS), MySQL, Kakao dev(login)
  • 협업 툴 : Git, Trello, Slack, Notion

3. 구현 기능

  • Kakao API를 이용한 소셜 로그인 / 회원가입
  • 숙소 위치, 특징, 사진 등 전반적인 정보를 등록 가능한 호스팅 기능
  • 숙소 전체 리스트 페이지 및 다중 필터링, 페이지네이션 기능
  • 숙소 상세페이지 보여주기 기능

4. 내가 구현한 기능

👆 요약

숙소에 대한 위치, 특징, 이름, 사진 등 전반적인 정보를 등록하는 호스팅 기능을 구현했으며, 몇 줄로 정리해보자면 아래와 같다.

  • pathname과 react router를 이용한 다수의 페이지 간 공통 레이아웃 및 페이지별 변경사항 적용
  • 카카오 지도 API를 이용한 주소 검색 기능 구현
  • state를 객체로 관리하여 공통된 정보 묶어서 관리 및 다른페이지에서 사용가능하도록 컴포넌트화
  • formData를 이용한 여러 장의 사진 업로드 및 Blob 객체를 활용한 사진 미리보기 기능 구현
  • useState및 local storage를 이용한 호스팅 정보 전역 관리
  • Front 개발 총괄로 프로젝트 초기세팅 및 모든 기능 연결, AWS 배포, 변수명 및 데이터 타입 한 판 정리, 공통 적용 필요핱 지식 문서화 및 공유

✌️ 상세 내용

🍊 1. React Router및 pathname을 이용한 다수이 페이지 간 공통 레이아웃 및 페이지별 변경사항 적용

첫 번째 Blocker로 다가왔던 것이 바로, hosting 하는 페이지를 별도의 여러 개의 페이지로 구현해야 한다는 점이었다. 실제로 airbnb 사이트를 보니, http://www.airbnb/hosting/amenitieshosting뒤에 등록하려는 정보가 스트링으로 URL에 붙어 나오는 것을 볼 수 있었다. 이렇게 하려면 아예 애초에 메인 페이지와는 다르게 /hosting이란 페이지들이 나오는 라우터가 추가로 있어야 한다는 소리! 그리고 약 11장이 넘는 호스팅 페이지들은 동일한 레이아웃을 사용하고 있었고, 오른쪽 정보 등록화면 및 왼쪽에 나오는 문구들만 다르게 나오고 있었다. 어떻게 구현해야 하나 한참을 고민했고, React Router공식문서에 나오는 Descendant<Routes>를 이용해 이 문제를 해결해 줄 수 있었다.

1-1) hosting라우터를 다음과 같이 별도로 관리해주었다.

Router.js에서 아래와 같이 처음부터 호스팅 라우터를 별도로 만들어준 후,

// Router.js
function Router() {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/*" element={<User />} />
        <Route path="/hosting/*" element={<HostingRouter />} />
      </Routes>
    </BrowserRouter>
  );
}

아래와 같이 호스팅 라우터 상에서 여러 정보를 관리할 수 있도록 해 주었다.

...중략
 <MainRight>
        <HostingNav />
        <Routes>
          <Route path="/" element={<StayType newStayInfo={newStayInfo} />} />
          <Route
            path="/location"
            element={<SearchForm newStayInfo={newStayInfo} />}
          />
          <Route
            path="/floor-plan"
            element={<FloorPlan newStayInfo={newStayInfo} />}
          />
          <Route
            path="/amenities"
            element={<Amenities newStayInfo={newStayInfo} />}
          />

1-2) 동일 내용 다른 컨텐츠는 다음과 같이 변수 및 객체로 관리해 주었으며, 기준점은 pathname으로 잡아주었다.

또한, 공통된 레이아웃인데 다른 내용이 나와야 하는 것들은 페이지 렌더링 되는 코드에 변수로 관리해주고, location.pathname을 이용하여 pathname에 따라 다른 정보가 보여지도록 조건에 따라 객체로 관리해주었다.

//화면에 렌더링 되는 부분
return (
    <MainLayout>
      {showVideo(location.pathname) ? (
        <MainLeftWithVideo>
          <BackgroundVideoContainer autoPlay muted loop type="video/mp4">
            <source src="/images/Hosting/Photographer.mp4" />
          </BackgroundVideoContainer>
          <Layer />
          <MainDescription>
            {getVideoPageTitle(location.pathname)}
          </MainDescription>
        </MainLeftWithVideo>
// 보여줘야 할 타이틀 객체로 관리
  const getVideoPageTitle = path => {
    const title = {
      '/hosting/photos': '이제 숙소 사진을 올릴 차례입니다.',
      '/hosting/title': '숙소 이름을 만들어주세요.',
      '/hosting/highlights': '숙소에 대해 설명해 주세요.',
      '/hosting/description': '숙소에 대해 더욱 자세히 설명해 주세요',
      '/hosting/price': '이제 요금을 설정하실 차례입니다',
    };
    return title[path];
  };

🍊 2. 카카오 지도 API를 이용한 검색 기능

외부 API와 Material UI를 사용하여 구현하였다. 카카오 지도 API는 기본적으로 자바스크립트로 되어있기 때문에 이를 리액트로 옮기는 과정이 신기하고도 재미있었으며, 자바스크립트 기본이 가장 중요하다는 것을 다시 한번 깨달았다.

  1. 유저가 검색창에 주소를 입력하면
  2. 검색한 위치에 마커가 찍히게 되며,
  3. 만약 해당 위치에 자세한 건물명이 있다면 건물명도 함께 나오도록 했다.
  4. 서버에 저장할 수 있도록 정확한 위도, 경도, 주소를 받아와 변수에 저장해놓았다.

🍊 3. 객체형 state로 정보 한 번에 관리 & 여러 곳에서 활용가능하도록 제작


버튼으로 인원수 관리하는 기능은 메인 페이지에도 있고, 상세 페이지 안의 예약기능 안에도 있기에 컴포넌트를 만든 후, 다른 팀원들이 가져다가 쓸 수 있도록 제작하고 싶었다. 따라서 state로 객체관리를 해주면서 상수데이터만 바꾸어 주면 다른 페이지에서도 동일한 UI와 기능을 활용할 수 있도록 만들었다.

const FloorPlan = ({ newStayInfo }) => {
  const [floorPlan, setFloorPlan] = useState({
    totalGuest: 0,
    bed: 0,
    bedroom: 0,
    bathroom: 0,
  });

  const minusOne = e => {
    const { name } = e.target;
    setFloorPlan(prevValue => ({ ...prevValue, [name]: floorPlan[name] - 1 }));
  };

  const plusOne = e => {
    const { name } = e.target;
    setFloorPlan(prevValue => ({ ...prevValue, [name]: floorPlan[name] + 1 }));
  };

  return (
    <PageContainer>
      <Container>
        {FLOORPLAN.map(({ id, text, name }) => {
          return (
            <FloorPlanOption
              key={id}
              text={text}
              name={name}
              floorPlan={floorPlan}
              minusOne={minusOne}
              plusOne={plusOne}
            />
          );
        })}
      </Container>
    </PageContainer>
  );
};

export default FloorPlan;

const FLOORPLAN = [
  { id: 1, text: '게스트', name: 'totalGuest' },
  { id: 2, text: '침대', name: 'bed' },
  { id: 3, text: '침실', name: 'bedroom' },
  { id: 4, text: '욕실', name: 'bathroom' },
];

🍊 4. 사진 미리보기 기능 구현

Blob객체를 이용하여 사진 미리보기 기능을 구현하였다. 개인적으로 이번 프로젝트에서 가장 신기했던 기능이었다! 우선 사용자가 사진을 올렸을 때, 올리지 않았을 때를 구분한 후, 만약 올렸다면 미리보기 컴포넌트를 보여주도록 틀을 만들었다. 사진 url의 경우 배열형태의 state로 담아, form에 onChange함수를 걸어주어, 만약 사용자가 사진을 올렸다면 배열을 state를 업데이트 하여 화면에 이미지가 보여지도록 만들어 주었다.
이 과정에서 하나 더 생각해야 했던 것은 input의 스타일링인데, 맨 처음에는 정말 안예쁘게 나온다!(일반적인 파일 선택이라는 기본 버튼이 있는 형태이다.) 따라서 이 부분은 스타일링 시에 실제 input은 안 보이도록 만들고, label을 활용하여 내가 원하는 형태로 스타일링을 해 주었다.

// 사진 미리보기 기능 전체
const Photos = ({ setPhotos }) => {
  const [images, setImages] = useState([]);

  const previewPictures = e => {
    const selectedFiles = [];
    const { files } = e.target;
    setPhotos(files);
    const targetFilesObject = [...files];
    targetFilesObject.map(file => {
      return selectedFiles.push(URL.createObjectURL(file));
    });
    setImages(selectedFiles);
  };

  return (
    <PageContainer>
      {images[0] ? (
        <PreviewWrapper>
          <PreviewTitle>어때요? 사진이 마음에 드시나요?</PreviewTitle>
          <ImgContainer>
            {images.map((url, index) => {
              return (
                <ImgHolder key={index}>
                  <PreviewImage alt="subPreview1" src={url} />
                </ImgHolder>
              );
            })}
          </ImgContainer>
        </PreviewWrapper>
      ) : (
        <ImageHolder>
          <ImageForm method="post" enctype="multipart/form-data">
            <ImageLabel htmlFor="pictureUpload">
              사진을 5장 이상 올려주세요
            </ImageLabel>
            <ImageInput
              type="file"
              id="pictureUpload"
              multiple={true}
              accept="image/*"
              onChange={previewPictures}
            />
          </ImageForm>
          <IconHolder>
            <AddPhotoAlternateOutlinedIcon style={{ fontSize: 60 }} />
          </IconHolder>
        </ImageHolder>
      )}
    </PageContainer>
  );
};

export default Photos;
// input 박스는 숨겨주고 label을 활용한다.
const ImageInput = styled.input`
  visibility: hidden;
`;

const ImageLabel = styled.label`
  margin-top: 10px;
  border: none;
  outline: none;
  background-color: transparent;
  font-size: ${({ theme }) => theme.fontSemiMedium};
  text-decoration: underline;
  cursor: pointer;
`;

🍊 5. localstrorage와 useState를 사용한 호스팅 정보 전역관리(최종 등록!)

👇 앞서 호스팅 기능을 통해 등록한 정보들이 아래와 같이 서버에 잘 들어가, 숙소 리스트페이지 및 상세페이지에서 모두 확인이 가능하다.

Redux를 배우기 전이었으며, 실제 홈페이지를 뜯어보니 localstorage에 내가 호스팅 하다가 중단하면 그 페이지까지의 정보가 로컬스토리지에 남는 것이었다. 안그래도 로컬스토리지를 프로젝트에서 사용해 본 적이 없는지라, 사용자가 다음 버튼을 누르면 로컬 스토리지에 해당 정보가 저장되도록, 그리고 이전을 누르면 그 페이지에서 저장했던 정보가 날아가도록 만들었다. 사진의 경우 추후 formData로 보낼 것이기에 state로 별도로 저장하였다가 전송하였다. 쇼핑몰의 장바구니 목록 만들고 삭제하는 기능 구현에서 영감을 받아 아래와 같이 만들어 보았다.

//호스팅 가장 첫 페이지에 오면 모든 정보를 로컬 스토리지에서 삭제했다.
  location.pathname === '/hosting' && localStorage.removeItem('stayInfo');

//계속해서 stayInfo라는 같은 키에 대한 값을 삭제해줬다가 업데이트 해주는 방향으로 만들어주었다.
  const addInfo = newStayInfo => {
    let info = [];
    if (localStorage.getItem('stayInfo')) {
      info = JSON.parse(localStorage.getItem('stayInfo'));
    }
    info.push(newStayInfo);
    localStorage.setItem('stayInfo', JSON.stringify(info));
  };

  const removeInfo = () => {
    let storageInfo = JSON.parse(localStorage.getItem('stayInfo'));
    storageInfo.pop();
    localStorage.setItem('stayInfo', JSON.stringify(storageInfo));
  };

사진의 경우 photos라는 state에 담아두었다가, 사용자가 업로드하면 해당 파일을 state에 담은 후, 추후 formData로 보냈다. 공식문서를 한참 확인한 후 반복문으로 같은 키값에 붙여 보내면 같은 키에 배열로 전달된다는 것을 알게되어. 백에서 배열로 처리가능하도록 아래와 같이 작성해 보았다.

//Hosting Router.js에서 state만들기
const [photos, setPhotos] = useState([]);

// photos.js에서
const { files } = e.target;
    setPhotos(files);

// photos에 담았던 사진을 formData로 전송하기(stayType이란 정보도 함께 전송)
const createFormData = storageInfo => {
    const stayType = storageInfo[0].stayType;  
    const formData = new FormData();

    formData.append('stayTypeID', stayType);
  // 여러 장의 사진이기에 아래와 같이 반복문 사용
    for (let i = 0; i < photos.length; i++) {
      formData.append('image', photos[i]);
    }
    sendData(formData);
    localStorage.removeItem('stayInfo');
};

// formData 보내는 코드
const sendData = formData => {
    const token = localStorage.getItem('dollharu');
    fetch(API.hosting, {
      method: 'POST',
      headers: {
        Authorization: token,
      },
      body: formData,
    })
      .then(res => res.json())
      .then(res => {
        res.message === 'SUCCESS'
          ? navigate('/hosting/registered')
          : reRegister();
      });
  };

6. 프론트 팀원들과 모두 모여 진행한 변수 한 판 정리, 백과의 소통 효율성 극대화!

프로젝트는 Scrum방식을 이용하여 agile하게 진행하였다. 서로의 진행사항은 Trello를 통해 확인할 수 있도록 하였으며, 회의록과 프로젝트 팀원 전체가 알아야 하고, 수시로 확인해야 하는 사항은 Notion에 기록하여 소통의 효율성을 극대화하도록 했다.

트렐로 활용시에 주의점은, 제목을 잘 정하는 것인데, 그 중에도 특히 어디까지 해야 Done인 것인지에 대한 논의가 잘 이루어져야 했다. 작업하고 있는 것은 in progress에 PR을 올려서 검토중이라면 PR에, 완벽히 머지까지 이루어졌다면 FE or BE Done에 카드를 넘겨 서로의 진행사항을 파악하도록 했다.

또한 노션을 통해 아래와 같이 Front가 다같이 알아야 하는 내용이 있다면 공유할 수 있도록 만들었다. 특히 styled-componentsMui, 지도 API등은 서로 다 처음 활용해 보는 것이었는데, 어쩌다보니! Front-end lead로서 이런 지식들을 빠르게 찾아 먼저 적용해 보고 팀원들과 공유한다면 팀원들이 조금 더 코드를 작성하는 데에 시간을 쓸 수 있겠다는 생각이 들어 아래와 같이 자료를 정리하여 공유하였다.

추가로, 전체 플로우 상 내가 등록한 정보를 숙소 전체 리스트페이지와 상세리스트 페이지에서 서버에서 받아오는데, 변수명이 같다면 서로의 코드를 이해하기도, 유지보수하기도 쉬울 것 같아 Front팀원들을 다함께 모아 변수명 한판을 작성한 후 Backend와 공유하였는데, 실제로 많은 소통의 번복을 줄일 수 있었다. (지난 프로젝트를 하며 이게 꼭!! 필요하다는 것을 느끼고 프로젝트 시작하자마자 적용해야 겠다고 생각했는데, 이로써 피드백의 중요성을 다시 한 번 느꼈다.)

5. 피드백 (좋았던 점, 개선할 점)

🥳 좋았던 점

  • 처음에 다같이 모여 서비스 플로우를 같이 그려본 것이 정말 좋았다. 1차 프로젝트 후, 피드백 했던 내용 중 하나가 프로젝트 시작 전 꼭 전원이 모여 프로젝트 플로우를 그려보는 것이었는데, 이번에 다함께 모델링을 진행하고 프로젝트 플로우를 그려보는데 거의 2일이 썼다. 처음에는 시간을 너무 많이 썼나하는 생각도 있었지만 결론적으로는 그게 맞았다. 데이터흐름이 어떻고 그에 따른 유저 플로우는 어떤지 사전에 논의하고 나니 각자 개발이 훨씬 수월하고 프-백 소통도 원활했다.
  • 후회가 남지 않는 프로젝트다. 프로젝트 기간 동안 전력을 다해 코드를 작성했다. 하루에 거의 4시간씩, 프로젝트 막바지에는 2시간도 못 자며 코드를 작성해보았다는 측면에서 후회가 남지 않는다. 이보다 시간을 더 많이 투자할 수는 없었다! 프로젝트 기간에는 사실 밥먹을 때에도, 샤워를 할 때에도 코드를 어떻게 구현해야하는가에 대한 생각만 했다. 그토록 몰입할 수 있었고, 몰입하는 과정이 너무너무 재미있었던 프로젝트다.
  • 스스로 성장했음을 느낄 수 있었다. 프로젝트 초반에 라우팅을 정리하고, styled-componets + mui를 사용하고 사진, 지도 기능 등 처음 해보는 것들을 해보느라 시간을 많이 썼다. 그러다 보니 나중에 글자 수 세기 기능, 숙소 가격 등록 기능 등을 하루 전날에 급하게 했어야 하는 상황이 왔는데, 집중해서 금방 뚝딱뚝딱 만들어버리는 나 스스로를 보고 감격했다. 😭 몇 달 전만 해도, 글자수 세기 기능을 만들지 못해 구글에 검색했었는데 이제는 그런 것 쯤이야 정말 뚝딱! 만들어버리는 스스로를 보고, 역시 개발은 시간 투자, 나의 노력이 헛되지 않았음을 확인하는 순간이었다.

😭 아쉬운 점

  • 시간이 부족했던 점이 아쉽다. 이번에 styled-components나 라이브러리 들을 처음으로 써보면서 프론트가 새로운 기술에 적응하는데 시간이 조금 걸렸다. 새로운 것을 빠르게 공부하고 적용할 수 있는 스킬을 정말 많이 늘리고 싶은데, 그러려면 아무래도 공식문서 보는 스킬을 늘릴 수 밖에 없는 것 같다. 막판에는 시간 부족으로 에어비앤비를 빌려 2박을 했는데, 그것 또한 엄청난 추억이다.
  • 리덕스를 알았다면 호스팅 정보 전역관리를 더욱 효율적으로 할 수 있었을 것 같다. 리덕스를 몰라서 호스팅 정보를 로컬스토리지에 관리했는데, 그것보다는 리덕스로 관리하는게 훨씬 좋을 것 같다는 생각이 든다. 최근에 Context API를 리액트 네이티브에서 사용해 보면서 편리함을 많이 느꼈다. 특히 API호출도 Provider에서 해 버리고 가져다 쓰면 전체적인 컴포넌트 코드도 짧아지고 가독성이 좋아지는 것을 보면서 호스팅 기능을 다시 리팩토링 해보고 싶다는 생각이 든다. 기업협업 프로젝트 마무리 하고 꼭! 리덕스를 활용해서 리팩토링을 해볼 것이다.

함께 한 우리 팀원들이 너무 소중하고 그리울, 아니 벌써 그립다..😥 1차도 그렇고 2차도 그렇고, 팀에 큰 트러블 하나 없이 프로젝트를 할 수 있었던게 너무 신기했다. 참 나는 인복 넘치는 사람이다. 프로젝트를 돌아보며, 결과물도 결과물이지만 함께한 사람들이 너무 좋았기에 더욱 감사하다. 돌하룻밤 포 레 버..✨

profile
⚓ A smooth sea never made a skillful mariner

0개의 댓글