Old People with Active Life
React, TypeScript, REST API를 활용한 쇼핑몰 프로젝트
인생의 제 2막을 시작한 5060세대의 여가 및 건강 활동을 위한 공간을 소개·대여하는 공간 대여 플랫폼
패스트캠퍼스 프론트엔드 개발 부트캠프 5기
개발 기간 : 2023.05.30 - 2023.07.02
배포 링크 : OP-AL
Repository : OP-AL
주하림 | 이시우 | 이은지 | 윤금엽 | 강동훈 |
GitHub 팀장 초기 개발 세팅 리덕스 설정 마이 페이지 (계좌, 구매내역) |
전체 디자인 제품 검색 기능 검색 리스트 |
로그인 회원가입 마이 페이지 (내 정보) |
제품 상세 페이지 제품 결제 페이지 |
더미데이터 수집 |
$ git clone https://github.com/OP-AL/opal.git
$ npm install
$ npm run start
1️⃣ 메인 페이지
-
제품 검색, 이벤트 배너, 지금 뜨는 곳, 퀵메뉴(예약 정보 확인)
2️⃣ 제품 페이지
-
제품 리스트, 상세 페이지, 결제 페이지
3️⃣ 마이 페이지
-
내 정보 수정, 내 계좌 관리, 구매 내역 조회
4️⃣ 인증
-
로그인, 회원가입
보기
📦 OP-AL
├─ .eslintrc.js
├─ .prettierrc
├─ 404.html
├─ README.md
├─ package-lock.json
├─ package.json
├─ public
│ ├─ _redirects
│ ├─ favicon.ico
│ ├─ index.html
│ ├─ manifest.json
│ └─ robots.txt
├─ src
│ ├─ App.tsx
│ ├─ Assets
│ │ └─ Images
│ │ ├─ 0_partyRoom.png
│ │ ├─ 10_hall.png
│ │ ├─ 11_gallery.png
│ │ ├─ 1_seminarRoom.png
│ │ ├─ 1_seminarRoom2.png
│ │ ├─ 2_filmStudio.png
│ │ ├─ 3_practiceRoom.png
│ │ ├─ 4_livebroadCast.png
│ │ ├─ 5_publicKitchen.png
│ │ ├─ 6_athleticFacilities.png
│ │ ├─ 7_rentalStudio.png
│ │ ├─ 8_classRoom.png
│ │ ├─ 9_cafe.png
│ │ ├─ d.jpeg
│ │ ├─ donghun.png
│ │ ├─ eunji.png
│ │ ├─ event_001.jpeg
│ │ ├─ event_002.jpeg
│ │ ├─ event_003.jpeg
│ │ ├─ event_004.jpeg
│ │ ├─ geumyeop.png
│ │ ├─ github.png
│ │ ├─ harim.png
│ │ ├─ now_001.jpeg
│ │ ├─ now_002.jpeg
│ │ ├─ now_003.jpeg
│ │ ├─ now_004.jpeg
│ │ ├─ now_005.jpeg
│ │ ├─ readme-item.gif
│ │ ├─ readme-main.gif
│ │ ├─ readme-mypage.gif
│ │ ├─ readme-sign.gif
│ │ └─ siwoo.png
│ ├─ Components
│ │ ├─ Common
│ │ │ └─ NotFound.tsx
│ │ ├─ Contents
│ │ │ ├─ Category.tsx
│ │ │ ├─ Counter.tsx
│ │ │ ├─ Event.tsx
│ │ │ ├─ QuickMenu.tsx
│ │ │ ├─ SearchBar.tsx
│ │ │ ├─ SelectionAccount.tsx
│ │ │ └─ VoucherModal.tsx
│ │ ├─ Footer
│ │ │ └─ Footer.tsx
│ │ └─ Header
│ │ └─ Header.tsx
│ ├─ Pages
│ │ ├─ Home
│ │ │ └─ Home.tsx
│ │ ├─ MyPage
│ │ │ ├─ MyAccount.tsx
│ │ │ ├─ MyInfo.tsx
│ │ │ ├─ MyPage.tsx
│ │ │ └─ MyPurchase.tsx
│ │ ├─ ProductDetail
│ │ │ └─ ProductDetail.tsx
│ │ ├─ ProductList
│ │ │ ├─ ProductItem.tsx
│ │ │ └─ ProductList.tsx
│ │ ├─ ProductPayment
│ │ │ └─ ProductPayment.tsx
│ │ ├─ Routes.tsx
│ │ ├─ SignIn
│ │ │ └─ SignIn.tsx
│ │ └─ SignUp
│ │ └─ SignUp.tsx
│ ├─ Styles
│ │ ├─ Category.scss
│ │ ├─ Event.scss
│ │ ├─ Footer.scss
│ │ ├─ Globalstyle.ts
│ │ ├─ Header.scss
│ │ ├─ Home.scss
│ │ ├─ Modal.scss
│ │ ├─ MyAccount.scss
│ │ ├─ MyInfo.scss
│ │ ├─ MyPage.scss
│ │ ├─ MyPurchase.scss
│ │ ├─ NotFound.scss
│ │ ├─ ProductDetail.module.scss
│ │ ├─ ProductItem.scss
│ │ ├─ ProductPayment.module.scss
│ │ ├─ QuickMenu.scss
│ │ ├─ SearchBar.scss
│ │ ├─ SelectionAccount.scss
│ │ ├─ Sign.scss
│ │ └─ common.scss
│ ├─ api.ts
│ ├─ index.tsx
│ ├─ react-app-env.d.ts
│ ├─ redux
│ │ ├─ reducer
│ │ │ └─ reducer.tsx
│ │ └─ store.tsx
│ └─ reportWebVitals.ts
└─ tsconfig.json
메인 페이지 (로그인 전) |
메인 페이지 (로그인 후) |
---|---|
로그인 | 회원가입 |
---|---|
메인 페이지 퀵메뉴 (로그인 후) |
제품 검색 페이지 |
---|---|
제품 상세 페이지 | 제품 결제 페이지 |
---|---|
마이 페이지 (내 정보) |
마이 페이지 (내 계좌) |
---|---|
마이 페이지 (구매 내역) |
마이 페이지 (구매 취소) |
---|---|
제품 검색 페이지에서 제품 클릭 시
Link 컴포넌트에서 URL 경로에 동적인 제품 ID
를 담고,
Route path
를 통해 ProductDetail.tsx에 제품 ID를 파라미터로 전달한다.
제품 상세 페이지에서는useParams()
를 이용해 파라미터 값을 받아와 api에 호출하여 상세 데이터를 출력한다.
useParams()
Hook으로 제품에 대한 페이지를 개별적으로 생성하지 않고,
변경되는 파라미터만 전달하여 api 호출을 할 수 있다.
// ProductItem.tsx
<Link
to={`/productdetail/${list.id}`}
key={index}
className="productItem__info-container"
>
// Routes.tsx
<Routes>
<Route path="/productlist" element={<ProductList />} />
<Route path="/productdetail/:id" element={<ProductDetail />} />
...
</Routes>
// ProductDetail.tsx
const { id } = useParams();
api를 제공해주신 강사님께서 회원가입 시 사용자의
accessToken을 Local Storage에 저장하는 방법
을 추천해주셨는데, Local Storage에서 accessToken을 관리하다 보니 멘토님과의 점검에서 문제가 발생하였고 멘토님께서cookies에 저장하는 방법
을 권유해주셨다.
(브라우저의 탭이나 창을 닫았다가 열었을 때로그인 상태가 그대로 유지됨
=> 보안성 문제 有)
현업에서는 session Storage나 cookies를 이용하는 경우가 많다고 답변 해주셨고,
회원가입 기능을 담당하지는 않았지만 상세 페이지 및 결제 페이지에서도 상태 관리로 사용하던
Redux에 문제가 생겨 cookie를 사용했으니 추가적인 학습을 해두면 좋을 것 같다.
// SignUp.tsx
const [cookies, setCookies] = useCookies(['accessToken']); // 변수로 지정한 cookie의 이름으로 초기화
async function handleSignUp(event: FormEvent) {
// ...
try {
const signUpData = await signUpApi(requestBody);
await authenticateCheck(signUpData.accessToken);
const accessToken = signUpData.accessToken; // 사용자의 accessToken을 변수로 지정
setCookies('accessToken', accessToken, { path: '/' }); // 쿠키의 도메인을 최상위 경로로 지정
// ...
}
}
// ProductDetail.tsx
const [accessCookies] = useCookies(['accessToken']);
const accessToken = accessCookies.accessToken;
const [dateCookies, setDateCookies] = useCookies(['selectedDateTime']);
const [productInfoCookies, setProductInfoCookies] = useCookies([
'productInfo',
]);
// ...
const handleProductPayment = () => {
if (accessToken) {
setDateCookies(
'selectedDateTime',
{
startTime: startTime,
endTime: endTime,
timeDiffer: timeDiffer,
},
{ path: '/' }
);
// ...
}
// ...
}
이번 프로젝트 시작 전, JavaScript와 React 학습이 부족한 상황이었고 일주일 정도 빠르게 학습을 진행했었다.
map()
함수는 배열의 각 요소를 순회하면서 새로운 배열을 반환하기 때문에 반복문에 사용해보게 되었다.
구현 초반에'상세 페이지는 제품 ID마다 다르게 출력되어야 하니 반복문으로 사용해주면 되는게 아닐까?'
라고 생각했고, Route path의 파라미터에/:id
로 지정하고 제품 조회 api 호출 결과에 map() 함수를 적용했지만 제대로 출력이 되지 않았다.
조원분들께 여쭤봤을 때 '변수의 상태를 보고 싶다면console.log
로 확인해보세요'라는 팁과 함께
map() 함수를 잘못 사용하고 있다는 것을 알게 되었다😭
(동적인 제품 ID를 파라미터로 전달해주고 출력해주기만 하면 되는데,
배열 속에 모든 제품 데이터가 넘어오는 구조로 잘못 생각했던 것...
)
(조원분의 답변대로 map() 함수를 지워주자 파라미터로 전달된 ID에 해당하는
제품 상세 데이터가 바로 잘 출력되었다.)
map() 함수가 잘 동작하기 위해선'배열 속에 데이터들이 존재해야 한다'
라는 점을 다시 깨닫고, 반복문에 적용해보게 되었다. map() 함수를 이용해 컴포넌트를 렌더링할 때컴포넌트의 변화를 추적하고 더 효율적으로 렌더링
하기 위해 고유한 식별자인key
를 props로 가져야 하는데 처음엔 key 값으로 왜 id를 받아야 하는지 이해가 되지 않았지만, 반복적으로 사용해보면서배열의 요소가 가진 속성 중 유니크한 값이 key가 되어야 컴포넌트들을 서로 구분하고 변경된 커포넌트만 재렌더링 할 수 있다
는 점을 이해하게 된 것 같다.
(꼭 id가 아니어도 되지만 index는 지양하기!)
// ProductDetail.tsx
// 제품의 tags를 가져와 해시태그 기호를 붙여 출력
const hashTags = product.tags.map((tag) => `#${tag}`);
{hashTags.map((tag, i) => (
<Space wrap key={i}>
<Button
type="primary"
shape="round"
size={'large'}
className={styles.product__tag}
>
<span>{hashTags[i]}</span>
</Button>
</Space>
))}
<div>
{/* 제품(공간) 장소 출력 */}
<h3 className={styles.product__address}>주소</h3>
{productAddress.map((item, i) => (
<p key={i}>{productAddress[i]}</p>
))}
</div>
이전 개인 과제들까지는
JavaScript 내장 함수인 fetch
를 이용해 api와 데이터를 주고 받았다.
fetch는 Promise를 반환하고, json() 메서드로 파싱이 필요하며, Node.js에서 동작하지 않기 때문에
axios를 많이 사용하고 있고 멘토님께서도axios를 자주 사용해보길 추천
해주셨다.
그래서 이번 프로젝트에서 axios로 api 호출을 하기로 결정했고 코드들을api.ts 파일
에 작성하였다.
api 관련 코드들을하나의 파일에서 볼 수 있다
는 장점이 있지만, 코드가 길어지다 보니가독성이 떨어져서
기능별로 api를 분리하여 파일을 관리해주는 방법이 더 좋겠다
는 생각을 하게 되었다.
// api.ts
export async function productDetailApi(id: string) {
const res = await axios.get(`${BASE_URL}/products/${id}`, {
headers,
});
const data: ProductDetailResponseData = res.data;
return data;
}
// ProductDetail.tsx
useEffect(() => {
const fetchProduct = async () => {
try {
const productDetailData = await productDetailApi(id as string);
// ...
}
};
fetchProduct();
}, [id]);
// ProductPayment.tsx
try {
const requestBody: PaymentRequestBody = {
productId: productInfo.id,
accountId: pickedAccount,
reservation: {
start: cookieSavedDate.startTime,
end: cookieSavedDate.endTime,
},
};
// 제품(공간) 구매 신청(결제)
await productPaymentApi(accessToken, requestBody);
// ...
// 제품 거래(구매)신청 - POST
export interface PaymentRequestBody {
productId: string; // 거래할 제품 ID (필수!)
accountId: string; // 결제할 사용자 계좌 ID (필수!)
reservation: {
// 예약 정보
start: string; // 예약 시작 시간(ISO)
end: string; // 예약 종료 시간(ISO)
};
}
프로젝트 시작 전 빠르게 React를 학습하면서
컴포넌트에 Lifecycle이라는 인생 주기가 있고 작성해둔 특정 코드가 실행될 시점을 지정할 수 있어
html 렌더링이 빠른 사이트를 구현할 때 유용하다는 점을 알게 되었다.
useEffect()
를 이용해 컴포넌트가mount
,unmount
,update
되는 시점 중 동작하는 시점을 조절할 수 있고,
두 번째 파라미터로의존성 배열
을 받아 그 요소(변수, state)가 변경될 때만 useEffect() 내에 작성한 코드가
실행되는데 React로 페이지를 구현하다보니 필수불가결적인 함수라는 사실을 알게 되었고, 도입하게 되었다.
// ProductDetail.tsx
useEffect(() => {
const fetchProduct = async () => {
try {
// URL 파라미터로 받은 ID를 매개변수로 담아 단일 제품 상세 조회 api 호출
const productDetailData = await productDetailApi(id as string);
// api 호출 결과로 받아온 데이터를 Redux로 상태 관리 중인 updateProductDetail의 상태로 업데이트
dispatch(updateProductDetail(productDetailData));
} catch (error) {
console.log(error as Error);
}
};
fetchProduct();
}, [id]); // 의존성 배열로 id를 지정해주어 제품의 고유한 ID가 바뀔 때마다 api를 재호출하도록 함
프로젝트 설계 과정 중 React로 현업에서 개발을 해보셨던 조원분께서
'상태 관리 라이브러리로 Redux를 많이 사용하던데, 이번 프로젝트에 도입 해보면서 같이 익혀보면 좋겠다'
는 제안을 해주셔서 Redux로 처음 상태 관리를 도전해보게 되었다.
props로 넘겨주는 useState에 비해 사전에 학습할 내용이 많아 더 간결하다는redux-toolkit
으로 진행했고
GitHub 팀장을 담당하셨던 조원분께서 redux 기본 세팅을 해주시고 사용해보면서 감을 익히게 된 것 같다.
(하지만 아직도 개념에 대해서는 이해가 가지 않아 추가적인 학습이 필요하다.)
// redux > store.tsx
import { configureStore } from '@reduxjs/toolkit';
import reducers from './reducer/reducer';
// reducer.tsx 파일에서 내보내진 reducer들을 configureStore에 전달하여 하나로 결합합니다.
const store = configureStore({
reducer: reducers,
});
// redux > reducer > reducer.tsx
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
interface ProductState {
id: string;
title: string;
// ...
단일 제품 상세 데이터, datePicker에서 선택한 입실 및 퇴실 시간, 인원 수, 선택한 계좌를 Redux로 관리 했는데
'제품 결제 페이지를 새로고침할 경우 제품 상세 페이지에서 선택한 옵션들이 초기화되는 문제'
를 발견했다😭
재렌더링이 되더라도 상태를 초기화 하지 않고 유지해주는 라이브러리가 있다는 얘기를 들었지만
마감 날짜가 촉박하여 Redux로 상태 관리를 하고 있던 변수들을cookie로 변경
하게 되었다.
조원분이 작성해주신 코드가 merge된 내용들도 있어 관련 코드들은 리팩토링이 반드시 필요할 것 같다😭
이전까지 개인 과제들만 수행하다 진행하는 첫 팀 프로젝트고 JavaScript와 React 학습을 많이 못한 상태라 민폐를 끼칠까 걱정이 많이 됐다. 프론트엔드 팀원들끼리 기획, 설계, 디자인, 개발을 모두 진행해야 해서 아이디어 회의에 약 일주일을 투자했는데 덕분에 프로젝트 전반적으로 이해가 잘 됐던 것 같고,
개발자라고 개발에만 집중할 게 아니라 트렌드나 디자인에도 관심을 많이 가져야겠다
는 생각을 하게 되었다.
아직 미숙하지만 JavaScript 문법이나 React에 대해 보다 친숙해질 수 있었고, 모르는 부분이 생긴다면 생각만 할 것이 아니라 코드 곳곳에console.log
를 찍어보며 구현 과정 중어느 부분에서 문제가 생겼는지 확인해보는 방법도 도움이 많이 되고 검색을 잘하는 것도 센스와 실력이 필요함
을 느끼게 되었다. 그리고 이전엔코드를 어디서부터 어떻게 봐야할지 감이 안잡혀
혼란스러웠는데,조금이나마 '숲'에서 '나무'를 보는 방법
을 익히게 됐다.GitHub repository로 협업하며 특강에서 학습했던 명령어들에 굉장히 익숙해졌으며
stash를 활용하는 방법을 조원분께서 알려주셔서 서로 수월하게 작업할 수 있었고, 충돌을 겪은 적이 거의 없었던 것 같다.
Redux로 관리하던 변수들을 cookie로 대체하거나, 결제 과정 중 출력되는 Modal 등은 다른 조원분들이 코드를 합쳐주신 내역들이 있어서 리팩토링을 통해 hooks 분리, 코드 정리, 주석으로 코드에 대한 설명 추가 등의 작업을 진행해봐야겠다💪🏻
보기
상세 페이지에서 선택한 입실 및 퇴실 날짜, 시간, 이용 시간, 예약 인원 수가 결제 페이지로 잘 출력되지만 새로고침 시 redux-toolkit으로 관리하던 state들이 undefined가 되는 현상이 발생해서 프로젝트 후반에 다른 조원분이 cookie로 변경을 하셨었다. 찾아보니
redux에서 페이지를 새로고침 할 경우 state들을 관리하는 store가 초기화되기 때문
에 발생하는 에러였다. 이런 점을 해결하기 위해redux-persist
라이브러리를 이용해 Local이나 Session Storage에 저장하는 방법을 도입해보았다.
상세, 결제 페이지에서 사용하는 slice(state)들만 파일을 분리하여 관리하는 방법을 시도했지만 새로고침 시 계속 초기화가 되었다😭 공식 문서와 검색, chatGPT를 이용해 5-6시간 동안 수정해보았지만 해결되지 않았는데 핵심은 공식 문서대로 적용하는 것이었다!
![]()
slice별로 파일 분리를 하고 각 slice의 reducer에 persistReducer()를 적용하고 combineReducers()를 적용할 땐 세션 스토리지에 저장도 되지 않았다. 그래서분리했던 slice들을 프로젝트 제출 당시처럼 reducer.tsx 파일에 모두 합치고
, persistReducer()의 첫 번째 인자인persistConfig에 whitelist, blacklist 속성
으로 persist를 적용할/적용하지 않을 reducer들을 지정해주고 두 번째 인자에combineReducers()로 묶어준 rootReducer를 지정
해주는 것이 기본적인 사용법이자 놓치고 있던 포인트였다! 코드를 수정하니 상세 페이지에서 slice로 관리하고 있는 내용들이 바뀔 때마다 잘 업데이트 되는 것을 session storage에서 바로 확인할 수 있었다😊 그리고결제 페이지를 새로고침할 때 이제 더이상 store가 초기화되지 않는다!
시간은 오래 걸렸지만 너무 뿌듯하다🙌🏻 이외에도직렬화 오류 해결을 위한 getDefaultMiddleware 함수에 예외처리
등의 내용들도 배울 수 있었는데, 잘 정리해둬야겠다!
ProductDetail 컴포넌트엔 상세 페이지 출력과 관련된 내용을 작성하고, 추가적인 로직은 분리해주고 싶었다. 그래서
DatePicker와 InputNumber는 예약 옵션들을 선택하는 컴포넌트들이기 때문에 따로 분리
를 해주었다. '예약하기' 버튼을 클릭했을 때로그인 상태를 확인하고 결제 페이지로 이동하는 로직도 분리
해주고 싶었는데,useNavigate()
나useDispatch()
등의 Hook은react 컴포넌트 내에서만 사용이 가능하기 때문
에 util 함수 내에서 사용하게 되면 hook call 에러가 발생했다. 그리고 로그인 상태를 확인하는useCookies()
hook도 react 컴포넌트의 최상단에 선언해줘야 하기 때문에 util 함수로 분리해보았지만 에러가 많아 ProductDetail 컴포넌트에 유지하는 방법을 선택했다. 다음에 더 공부를 하고나서 도전해보고 싶다😭
그리고 DatePicker의입실 및 퇴실 날짜, 시간을 의미하는 dates
의 타입을 any로 지정해줬던 부분을 명확하게 지정해주었다. antd의RangePickerProps는 DatePicker의 속성을 정의한 타입
이므로 'value'로 지정해주었다. 그리고선택을 하지 않았을 경우 value가 null일 수 있다
는 타입 에러에 start, end를 옵셔널로 수정해주어도 redux에서 dispatch로 업데이트를 할 때 데이터가 분명하게 있어야 하기 때문에 에러가 유지되었다. 그래서 방법을 찾아보고start와 end의 값이 있을 경우에만 로직이 처리되도록
조건문을 넣어줬더니 dispatch문 내에서도 null 타입 에러가 해결되었다!
프로젝트를 제출했을 때 담당해주셨던 멘토님께서 리뷰에
useMemo()
를 통한 메모라이징을 추천해주셨다. ProductPayment 컴포넌트에서는 useEffect()를 사용하지 않은 상태였는데, 상세 페이지에서 선택한 예약 옵션들을 연산을 통해 형태를 바꿔 출력해주는 로직이 많았다. 그런데 그 로직들이 컴포넌트 내에 작성되어 있었고, 결제완료 시 출력되는 Modal의 상태도 useState()로 관리를 해주고 있었기 때문에 Modal의 상태변화에 따라 다른 로직들이 재실행되는 문제가 있기 때문에미리 연산된 값을 메모리에 저장해두고 값이 변할 때만 로직이 재호출될 수
있게 해주는 useMemo()`를 사용해보길 리뷰에 남겨주셔서 도입해보았다! 앞으로 컴포넌트 내에 작성할 로직과 밖에 작성할 로직, 메모라이징에 대해서도 고려해야겠다는 새로운 점을 알게 되었다🏃🏻♀️
1차 토이 프로젝트 이후 두 번의 프로젝트 과정 중
GitHub issue를 사용하는 방법
을 배웠고 활용하면서 작업 현황과 to do list를 확인할 수 있는 점이 개인적으로 정말 좋다고 생각했다👍🏻 그래서 혼자 진행하는 리팩토링이지만 기록하면서 남길 수 있는 목적으로 issue를 생성하고,issue를 포함한 branch 작명 및 commit에 issue로 바로 이동할 수 있는 하이퍼링크를 적용
했다!
그리고 아직 실력이 부족해 코드를 열어보면 코드 파악에 시간이 오래 걸리기도 하고 프로젝트 진행 중 조원들과 회의를 하거나 질문을 주고 받을 때도주석으로 설명을 추가해두면 코드를 파악하고 이해하는 데에 시간을 절약할 수 있어서 좋다고 느꼈기 때문에 주석을 추가
했다. 개발자마다, 그룹마다 스타일은 다르겠지만앞으로 서로 잘 이해할 수 있는 코드를 구현하고 싶다
👩🏻💻
그리고 최근 프로젝트의 멘토님께서각 브랜치에서 작업이 완료되면 브랜치를 삭제
해주는 것이 repository 관리가 수월할 것이라고 추천해주셔서 리팩토링 진행 전 main 브랜치를 제외한 개발 브랜치들은 모두 삭제했다.(git Flow, gitHub Flow를 적용하지 않았기 때문에 가능했다!) PR 시 브랜치를 삭제하는 옵션도 있다고 하니 활용해봐야겠다!