- 🗓 기간: 2023.11.20 ~ 2023.12.01
- ❓ 주제: Next.js를 활용한 숙박 예약 서비스
- 🎉 배포 링크
팀장 | 남궁종민 | 팀원 | 박성후 | 팀원 | 서지수 | 팀원 | 장문용 | 팀원 | 정진주 |
---|---|---|---|---|
@NamgungJongMin | @HOOOO98 | @jseo9732 | @moonyah | @jinjoo-jung |
|
|
|
|
|
팀장 | 김진홍 | 팀원 | 김정훈 | 팀원 | 박찬영 |
---|---|---|
@deepredk | @Aleexender | @cyPark95 |
|
|
|
남궁종민
- 초기 개발환경 세팅 (절대 경로 alias 설정 / eslint, prettier 설정 / 디렉터리 구조 / api 요청 메서드를 반환하는 객체 설정)
- 검색엔진최적화를 위한 Metadatas 작성 (robots, sitemap, favicon, title, description)
- 로그인/ 회원가입 input 값 validation
- validation 결과에 따라 디자인 변경 및 버튼 활성화 여부 결정
- 각 input 컴포넌트 단위 리렌더링
- 반복되는 react hooks -> custom hooks로 분리 (useAuthInput, useButtonActivate)
- 단위 당 한번의 요청만이 갈 수 있도록 debounce를 함수에 적용
로그인 | 회원가입 |
---|---|
- input 값을 채운 후 버튼 클릭을 통해 api 요청을 할 때 더블클릭이나 단시간에 여러번의 클릭을 할 경우 여러번의 요청이 가능 이슈가 있었다. debounce를 적용하여 의도한 동작에서 한번의 요청만이 가도록 해결했다.
const signup = debounce(
async (
email: InputType,
password: InputType,
nickname: InputType,
phone: InputType
) => {
try {
const res = await authRequest.createUser({
email: email.value,
password: password.value,
nickname: nickname.value,
phone: phone.value,
});
console.log(res);
if (res.status === 'SUCCESS') {
router.replace('/auth/signin');
} else {
setSubmitError(res.errorMessage);
}
} catch (error) {
console.log(error);
}
},
200
);
- 각 input마다 value값의 변화를 상태로 저장하고 렌더링하는 코드가 반복되었고 validation 까지 하려고 하니 코드가 너무 지저분해지고 유지보수성이 떨어졌다. 각 input 별 관리와 validation까지 한번에 처리하는 useAuthInput이라는 custom hook으로 분리하여 해결했다.
const useAuthInput = (target: string, password?: InputType) => {
const [input, setInput] = useState({
value: '',
validationPass: false,
});
const handleChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>): void => {
if (target === 'email') {
setInput({
value: e.target.value,
validationPass:
/^[0-9a-zA-Z]([-_.]?[0-9a-zA-Z])*@[0-9a-zA-Z]([-_.]?[0-9a-zA-Z])*\.[a-zA-Z]{2,3}$/.test(
e.target.value
),
});
}
if (target === 'password') {
setInput({
value: e.target.value,
validationPass: /^(?=.*[a-zA-Z])(?=.*[0-9]).{8,15}$/.test(
e.target.value
),
});
}
if (target === 'passwordConfirm') {
if (password) {
setInput({
value: e.target.value,
validationPass: e.target.value === password.value,
});
}
}
if (target === 'name') {
setInput({
value: e.target.value,
validationPass: (input.validationPass =
e.target.value.length >= 2 && e.target.value.length <= 10),
});
}
if (target === 'contact') {
setInput({
value: e.target.value,
validationPass: /^\d{2,3}-\d{3,4}-\d{4}$/.test(e.target.value),
});
}
},
[input, target, password]
);
return [input, handleChange, setInput];
};
- 각 input값 입력시 해당 input만이 리렌더링되게 하려고 컴포넌트를 memo로 묶어주었지만 의도한대로 값을 입력하는 input 값만이 리렌더링되지 않았다. 커스텀 훅에서 생성되는 handleChange 함수가 여러번 생성되며 제대로 메모이제이션이 되지 않는다는 것을 깨닫고 useCallback으로 사용하는 커스텀 훅의 함수또한 메모이제이션 해줌으로써 원하는 결과를 얻을 수 있었다.
// inputEmail.tsx
const InputEmail = memo(({ email, handleEmail }: EmailProps) => (
<div className='relative my-5'>
<label htmlFor='email' className='text-base leading-10'>
이메일*
</label>
<input
type='text'
name='email'
id='email'
value={email.value}
placeholder='이메일을 입력해주세요.'
onChange={handleEmail}
required
autoComplete='off'
className='border-lightGray top-10 h-14 w-full rounded-[10px] border-2 p-4 text-base text-black'
/>
<ValidationIcon input={email} />
<ErrorMsg target='email' input={email} />
</div>
));
// useAuthInput.ts
const handleChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>): void => {
if (target === 'email') {
setInput({
value: e.target.value,
validationPass:
/^[0-9a-zA-Z]([-_.]?[0-9a-zA-Z])*@[0-9a-zA-Z]([-_.]?[0-9a-zA-Z])*\.[a-zA-Z]{2,3}$/.test(
e.target.value
),
});
}
if (target === 'password') {
setInput({
value: e.target.value,
validationPass: /^(?=.*[a-zA-Z])(?=.*[0-9]).{8,15}$/.test(
e.target.value
),
});
}
if (target === 'passwordConfirm') {
if (password) {
setInput({
value: e.target.value,
validationPass: e.target.value === password.value,
});
}
}
if (target === 'name') {
setInput({
value: e.target.value,
validationPass: (input.validationPass =
e.target.value.length >= 2 && e.target.value.length <= 10),
});
}
if (target === 'contact') {
setInput({
value: e.target.value,
validationPass: /^\d{2,3}-\d{3,4}-\d{4}$/.test(e.target.value),
});
}
},
[input, target, password]
);
-
서버 컴포넌트에서 로그인시 백단에서 set-cookie 해준 값을 읽어오지 못하는 이슈 발생. http only 속성이라 자바스크립트로 해당 쿠키에 접근할 수가 없었다. 또한 클라이언트 컴포넌트와 달리 서버사이드에서는 set-cookie해준 쿠키값을 가지고 있지 않기 때문에 미들웨어를 설정하여 쿠키값을 가로채거나 http only를 해제하는 방법을 생각하게 되었다. set-cookie를 해준다면 프론트에서 요청에 쿠키를 심어주지 않아도 알아서 담아갈 것이라 기대했던 바와 달라 이 두 방식도 올바른 해결방법이 아니라고 생각했고, 실제 현업에서 next.js app router를 쓸 때 어떤식으로 쿠키 인증을 하는지 피드백 때 여쭤보려고 한다.
-
로그인이 필요한 동작에서 로그인이 되어있지 않아 로그인 페이지로 리다이렉션 되었을 때 해당 페이지에서 로그인 한다면 루트 페이지가 아닌 해당 동작을 하려던 페이지로 돌아가게 구현하고 싶었다. 이를 위해 로그인이 될 경우 뒤로가기 동작을 하는 것이 어떨까 생각하게 되었지만, 첫 페이지가 로그인일 경우와 회원가입에서 로그인페이지로 왔을 경우 등 여러 예외 사항들이 많이 발생하였다. 이 부분도 피드백을 듣고 리팩토링 때 반영해야겠다고 생각했다.
백엔드와의 첫 협업이라 설레기도 했고, 걱정도 많았던 프로젝트였다. 백엔드를 제대로 경험해본적이 없기 때문에 내가 요구하는 사항들이 백엔드 팀에게 어느정도의 시간이 쓰이는지 감이 잡히지 않았고, 백엔드 팀 또한 마찬가지였다. 잘못하면 서로 감정이 상할 수도 있을 것이라 생각이 들었다. 서로의 상황을 부담없이 말하고 자유롭게 의견들을 공유하기 위해서는 단지 텍스트로 의사 전달을 하는 것이 아닌 서로가 직접 대화할 수 있는 순간이 많아야 한다고 느꼈다. 따라서 짧은 간격으로 화상 회의를 통해 의견을 조율했고, 테스트를 위한 중요한 미팅때는 오프라인 미팅을 통해 프로젝트를 진행했다. 덕분에 좋은 분위기로 프로젝트를 끝맺을 수 있었던 것 같다. 이번 프로젝트에서 가장 크게 느꼈던 것은 내 일이 아니더라도 어느정도 공감할 수 있는 정도의 지식을 가지고 있어야 개발의 긍적적인 진행이 가능하다는 것이었다. 내가 프론트기 때문에 프론트엔드 기술만을 공부한다면 제대로 협업할 수 없을 것이라 느꼈고, 개발 프로세스에 있어서 전체적인 그림을 알아두는 것이 앞으로 큰 도움이 될 것이라 생각했다. 이번 프로젝트 덕분에 프론트엔드 뿐만이 아니라 백엔드 팀들의 상황과 에로사항들을 알 수 있었고, 다음번 협업 때는 더욱 잘할수 있겠다는 자신감을 얻게 되었다.
추가로 팀장의 부담감이 심했었던 프로젝트였다. 익숙하지 않은 기술들로 개발을 진행하면서 내가 과연 팀원들을 리딩할 수 있을까라는 두려움도 있었다. 그러나 모든 부담을 내가 질 필요는 없었다. 성후님은 항상 자신감이 부족했던 나를 북돋아주었고, 지수님은 정말 든든하게 나의 부족한 부분들을 메꿔주셨다. 또 진주님은 팀의 분위기를 항상 밝게 해주셨고, 문용님 또한 소심한 내가 팀에 잘 적응할 정도의 분위기를 만들어주셨다. 프로젝트 결과뿐이 아니라 진정한 동료들을 얻을 수 있었던 것 같아 풍족한 프로젝트였다.
박성후
- 버튼 태그 속 이미지 태그 vs 버튼 백그라운드 이미지
// 버튼 태그 속 이미지 태그
<button>
<img/>
</button>
// 이 방법은 버튼 안에 이미지 태그를 직접 포함시키는 방법입니다.
// 버튼은 텍스트 또는 다른 콘텐츠와 함께 이미지를 포함할 수 있습니다.
// 이 방법을 선택하면 이미지와 속성을 조작할 수 있습니다.
// 버튼에 텍스트와 이미지를 함께 표시해야 하는 경우 첫 번째 방법이 유용할 수 있습니다.
// VS
// 버튼태그의 백그라운드 이미지
<button style={{'backGroundImage:'url(...)'}}/>
// 이 방법은 버튼에 배경 이미지를 추가하는 방법입니다.
// 버튼 텍스트나 다른 콘텐츠는 일반적으로 버튼 내에 추가됩니다.
// 이 방법을 선택하면 배경이미지와 스타일 속성을 조작할 수 있습니다.
// 반면에 버튼 전체가 이미지여야 하는 경우 두 번째 방법이 더 적합할 수 있습니다.
- 인풋 Placeholder vs label
<input placeholder="입력해주세요"/>
//placeholder 속성은 사용자가 입력할 내용에 대한 예시나 힌트를 제공하는 데 사용됩니다.
// 하지만 placeholder는 시각적인 힌트로만 제공되기 때문에
// 스크린 리더 사용자 등에게는 충분한 정보를 제공하지 못할 수 있습니다.
// VS
<label for="test">테스트 인풋</label>
<input/>
// 위의 예시에서 for 속성은 input 요소의 id 값과 일치시켜 어떤 입력 필드와 관련이 있는지 지정합니다.
// 이 방식을 사용하면 스크린 리더 사용자 및 시각적 디자인과 상관없이 명확한 설명을 제공할 수 있습니다.
비로그인 시 예약하기 | 로그인 시 예약하기 |
---|---|
비로그인 시 장바구니 | 로그인 시 장바구니 |
---|---|
객실 예약 유효성 검사 | |
---|---|
- 라이브러리 CSS override
rsuite? vercel사에서 제작한 라이브러리이기 때문에 리액트와 넥스트에 최적화되어 있습니다.
상황 : 숙박 날짜 선택을 위해 DaterangePicker를 가져와 사용했습니다.
문제 : 라이브러리를 사용할 때, 공식문서를 정독하지 않아 발생했습니다.
CSS 모듈이 같이 설치가 되어 사용했으나 라이브러리를 사용한 페이지를 방문하면 CSS가 override되어 다른 페이지도 레이아웃이 깨지는 현상이 일어났습니다.
해결 : 라이브러리 내에 이미 해결방안이 나와 있었습니다.
기존에 사용하던'rsuite/dist/rsuite.min.css';
대신'rsuite/dist/rsuite-no-reset.min.css';
를 사용하면 되었습니다.
느낀점 : 사실 이 문제도 직접 해결한 문제가 아니라 리팩토링 이후로 미룬 후에 조원 분이 찾아주신 해결책이었습니다. 앞으로는 어떤 것이든 공식문서를 꼼꼼히 보고 사용해야겠습니다.
이번 프로젝트에서는 검색 엔진 최적화(SEO), 스크린 리더를 사용하는 유저들의 웹 접근성을 고려해보기 위해 NEXT를 사용하자는 팀 의견에 동의 했다. 다만 문제는 NEXT에 대한 이해도가 떨어진 상태로 개발을 시작했다는 점이다. 단순히 서버 컴포넌트와 클라이언트 컴포넌트의 차이에 대해서만 알고 있었는데, 실제 프로젝트에서는 깊이가 더 깊어지고, 상호작용이 많아지기 때문에 얕은 지식만으로 개발을 진행하기가 어려웠습니다. 중간중간 필요한 내용은 공식문서, 블로그를 참고하며 공부를 했습니다. 앞으로도 새로운 환경에 지속적으로 노출 시켜 이런 성장을 이루어 나가야 겠다고 생각했습니다. 처음 백엔드 개발자 분들과 협업을 통해 느낀 점은 데이터 구조, API 명세 부분에서 확실하게 문서화를 하고 지속적으로 소통을 하여 오차가 없도록 해야 한다는 것을 느꼈습니다.
서지수
- 각 페이지에 맞게 사용할 수 있도록 컴포넌트화
- 장바구니 조회
- 장바구니에 담긴 상품 데이터 (이미지, 상품명, 옵션 등)에 따른 상품별 구매 금액, 전체 주문 합계 금액 등을 화면에 출력
- 지난 체크인 날짜, 재고 없음으로 인한 예약 마감 상품 표시
- 장바구니 개별 삭제 기능 구현
- 장바구니 체크 박스를 통해 삭제 기능 구현
- 예약 불가 상품 삭제 기능 구현
- 예약 마감 상품을 제외한 전체 선택 / 해제 기능 구현
- 체크 박스를 통해 결제할 상품을 선택/제외 기능 구현
- 장바구니에서 주문하기 버튼 클릭 시, 예약(주문) 페이지로 이동
헤더 | 장바구니 개별, 선택 삭제 |
---|---|
예약 불가 장바구니 삭제 | 전체 선택, 선택 항목 예약 |
---|---|
- 필요한 위치에서만 푸터 표시 NextJS 서버에서 푸터가 필요한 페이지인지 구분한 뒤에 렌더링이 되기 전에 푸터 유무를 판단하여 보여 주고 싶었는데 서버 컴포넌트의 header, cookie (from next/header)를 사용하여 정보를 받아와도 페이지를 판단할 수 있는 원하는 값을 찾을 수 없었다. 프로젝트 기한 때문에 필요한 페이지마다 푸터를 넣어주는 방식으로 임시 해결했지만 서버 컴포넌트와 클라이언트 컴포넌트의 차이에 대해서 공부할 수 있었다. 이후 리펙토링 과정에서 아쉬웠던 부분을 개선해보려고 한다.
- 장바구니 선택
장바구니에 예약 불가(체크인 날짜가 지났거나 예약 가능한 방의 수가 없는 경우) 항목은 체크가 불가능하게 처리, 전체 선택, 필요한 항목만 선택 후 삭제, 개별 삭제 등 고려해야할 경우의 수가 많아 많은 어려움이 있었다.
- Strict 모드로 인한 전체 선택 배열에 같은 아이템이 들어가 실제 선택한 수의 2배가 선택 처리되는 이슈
- 첫 렌더링 시 전체 선택이 될 때 각 checkbox의 onChange가 개별적으로 인식되지 않아 각 항목이 체크가 되었을 때 그에 따른 배열 값을 바꿔줘야하는 이슈
- 이 외에도 많은 이슈가 있었지만
useEffect
와useState
를 잘 고려하여 해결하면서 다시 리액트의 라이프 사이클을 공부할 수 있었다.
이전 토이2 프로젝트에서 익숙했던 페이지 라우터를 사용했었는데 이번 프로젝트에서 app 라우터를 사용하면서 app 라우터 개발 경험을 할 수 있었고 이전에는 고민하지 않았던 서버 컴포넌트와 클라이언트 컴포넌트에서 대해서 공부할 수 있었습니다.
백엔드와의 협업을 통해서 많은 개발이 진행되기 전에 빠르게 데이터 형식이나 api 문서를 통일한 뒤에 작업해야지 큰 문제 발생하지 않고 문제 해결도 수월하게 할 수 있다는 것을 알게 되었고 문서화와 소통의 중요성을 알게 되었습니다.
장바구니 기능 구현을 담당하면서 디테일한 작업들이 많아서 상태관리나 라이프 사이클을 공부할 수 있는 좋은 경험이 되었습니다. 코드의 가독성을 위해서 컴포넌트의 분리 및 컨벤션을 따르려고 노력했습니다. 팀원들과 대면으로 소통하여 원할하게 프로젝트를 마무리 할 수 있었습니다!
장문용
- main Carousel : autoplay 적용
- main Icon : 숙박 업소 카테고리 별 분류 아이콘, 분류 페이지와 연결되어 있다.
- main contents 01 → 지역 별 펜션 보여주기, API 연결
- main contents 02 → 지역 별 호텔 보여주기, API 연결
- main contents 03 → 지역 별 전체 숙소 보여주기 (분류 페이지와 연결)
- 카테고리 별 분류 드롭다운
- 지역 별 분류 드롭다운
- 숙소 카드 제작, infinite scroll 적용
- 숙박 업소 목록 조회 API 연결
- 숙소들은 각각의 detail page와 연결
메인페이지 carousel | 메인페이지 contents |
---|---|
카테고리 페이지 | 상세페이지로 이동 |
---|---|
호텔 카테고리와 지역을 동시에 분류해야 하는 상황에서, URL을 활용하여 페이지를 구성하는 과정에서 발생한 어려움이 있었다.
- 상황 : 처음에는 URL을 slug로 설정하여 호텔을 분류하려고 했으나, 이 방식이 너무 헷갈려서 로직을 변경하게 되었다.
- 문제 : slug를 사용한 URL은 category와 location의 명시성이 부족해 혼란을 초래해서 사용자가 원하는 정보를 정확히 식별하기 어렵다.
- 해결 : URL을
product?category=&location=
로 명시적으로 변경하여 각각의 매개변수를 명확히 나타내게 되었다. 사용자가 쉽게 필터링하고 원하는 정보를 찾을 수 있도록 개선이 되었다. - 느낀 점 : URL 구조의 중요성을 깨달았고, 명확한 매개변수를 통해 사용자 경험을 향상시키는 결정을 내렸다.
이 프로젝트를 통해 백엔드와 소통하면서 api 연결과 데이터를 활용하는 협업 경험을 하였습니다. 프론트 개발에서 넥스트와 타입스크립트를 사용하면서 코드의 가독성과 유지보수성을 향상시키고자 하였습니다. 담당했던 부분에서는 메인 페이지와 분류 페이지 간의 연결 및 사용자 경험을 개선하기 위해 신경을 썼던 것 같습니다. 팀원들 간의 적극적이고 활발한 소통으로 인해 짧은 기간이지만 무사히 프로젝트를 마무리 지을 수 있었습니다!❤️
정진주
- 주문 결제 페이지 , api 연결
- 주문할 숙소 정보 결제 페이지로 가져오기
- 이용자 , 예약자 정보 동일하면 예약자 정보 가져오기
- 이용자 정보, 결제 방식, 필수 체크박스 선택 후 결제 가능
- 주문 내역 상세 페이지 , api 연결
- 결제 완료 → 주문 완료 상세 페이지
- 결제 금액, 결제 수단, 이용자, 예약자 정보 보여주기
- 주문 내역 목록 페이지, api 연결
- 사용자가 숙소 결제한 날짜 기준으로 숙소 목록 띄어주기
- 상세보기 클릭시 상세 페이지로 이동
숙소 예약 정보 조회 & 결제 완료 | 결제(예약)했던 숙소 목록 조회 |
---|---|
장바구니 담은 숙소 2개 결제 | |
---|---|
-
Next.js SSR을 사용한 SEO를 위한 Next.js를 사용한 프로젝트가 이번이 처음이라서 그런지, 서버 컴포넌트와 클라이언트 컴포넌트 간의 렌더링의 차이점이나 props전달하는 방식들을 헤맸던 것 같습니다. 그러다 보니 서버 컴포넌트에서 useState, useEffect를 사용하게 됐고 결과 ‘use client’를 작성하라는 에러 메세지를 마주하면서 다시 서버컴포넌트와 클라이언트의 차이점을 제대로 공부하고 코드를 작성해야겠다고 생각했습니다. 이후 lifecycle hooks같은 상호작용성을 포함하는 컴포넌트라면 그것을 클라이언트 컴포넌트로 만들고, 그렇지 않으면 서버컴포넌트로 관리를 하는 방식으로 코드를 작성하면서 next.js를 왜 사용하는지, 어떤 부분에서 사용해야하는지 등 문제를 해결해나가며 배울 수 있었습니다.
이 부분은 완전히 해결하지 않은 상태이지만 겪은 문제이기에 적었습니다. 직역하면 Chunk 파일을 불러오지 못해서 발생하는 에러라고 합니다. 사용자의 브라우저에서 캐싱이 되었거나, 이전 버전의 페이지가 계속 열려있던가 하는 등의 이류로 인해 지금은 존재하지 않는 이전 저번의 chunk 파일을 요청하게 되면서 ChunkError가 발생하는 현상인데, 이 에러가 계속 뜨면서 화면이 보이지 않는 것이 아니라 열 번 중 한번꼴로 발생하고 새로고침하면 브라우저 화면은 잘 작동을 해서 정확한 에러 발생 이유는 찾지 못 했습니다. 어떤 이유로 에러가 발생하는지 대충 이해는 했지만 정확하게 해결을 한 것은 아니라서 더 찾아보고 문제를 해결해 나갈 예정입니다.
이번 프로젝트는 숙박 예약 서비스로, 제가 맡았던 부분인 숙소 결제 페이지와 주문 내역 목록 페이지간의 상호작용을 원활하게 하기위한 컴포넌트 구조를 나누고 코드를 작성하도록 노력하였습니다. next.js 프레임워크를 프로젝트에 처음으로 사용하면서 기존과는 달리 서버사이드렌더링을 통해 코드를 짜고 구조가 달라진 부분이 어렵다고 느껴졌지만 좋은 팀원분들을 만나서 next.js에 대해서 더 많이 배울 수 있었고, 백엔드분들과 협업하면서 API 문서를 보고 데이터 구조를 잡고, 데이터 정보를 가져오기까지 같이 소통하고 수정하는 과정을 거치면서 협업의 중요성도 다시 한 번 느낄 수 있었던 프로젝트였습니다.