프로젝트 리뷰 - codeAliveB2C
1. 완성화면 보러가기
[소스코드]
2. 개발 기간 및 기여
개발 기간:
- 2022년 11월 ~ 2023년 1월 18일(수) ※ 해당 프로젝트는 회사 사정으로 인해, 정식 운영되지는 않았음.
기여:
- 프론트(일반 사용자 / 관리자) 메인 개발
3. 프로젝트 제목 및 설명
제목:
- codeAlive-B2C 웹 구축
설명:
- codeAlive(Creverse와 Unity가 공동 개발한 파이썬 교육프로그램)의 사업 확장을 위한, B2C용 LMS
- root > front: 일반 사용자(학생, 학부모 등)가 이용하는 화면을 구성하는 소스코드
- root > admin: 관리자가 관리자 기능을 사용하기 위해 이용하는 화면을 구성하는 소스코드
4. 기술 스택 및 사용된 도구
언어 :
- JavaScript(ES6+): 최신 ECMAScript 표준을 사용한 프로그래밍.
프레임워크 및 도구:
- react: 프레임워크
- react-router: SPA(Single Page Application) 라우팅 관리
- react-redux: 전역 상태 관리
- react-quill: 사용자 에디터 1
- react-quilljs: 사용자 에디터 2
- pixi.js: 메인배너의 2D 웹 그래픽 효과에 사용
- antd: UI 컴포넌트로 사용
- apexcharts:사용자 학습정보의 차트와 그래프에 사용
백엔드 통신:
- axios: HTTP 클라이언트 라이브러리
- Postman: API 테스팅
배포:
- Azure StaticWebApp: Azure 클라우드의 정적웹 배포서비스
5. 다음 프로젝트에서 알아야 할 것들
1. 프로젝트 구조 잡기
- 리액트 공식문서를 참고한 결과, 리액트는 파일을 어떤 식으로 폴더에 분류할 것 인지에 대해 공식적으로 정해놓은 것은 없지만, 아래와 같이 몇 가지 인기 있는 접근법( (1) 파일의 기능 또는 라우트에 의한 분류, (2) 파일 유형에 의한 분류 )이 있다. 여기서 나는 ‘라우트에 의한 분류’ 를 선택하여 프로젝트의 폴더와 파일을 관리하고자 한다.
-
Common/ 경로에 있는 파일들은 각 라우트에서 공통적으로 사용하고, 변하지 않는 기능들로 다른 프로젝트에서도 반복적으로 재사용 가능하다.
- [리액트 문서 링크]
[라우트에 의한 폴더와 파일의 위계 구조]
-
src/
- index.js
-
App.js
-
MyPage/
- Navigation.js
- Dashboard.js
- DashboardSlice.js
- MyClassroom.js
- MyClassroomSlice.js
- OrderBasket.js
- OrderBasketSlice.js
- OrderHistory.js
- OrderHistorySlice.js
- QA.js
- QASlice.js
-
MainPage/
- Mask.js
- StarWarp.js
- MainBanner.js
- VideoWrap.js
- Programs.js
- ThirdLine.js
- FourthLine.js
- FifthLine.js
-
Introduction/
- Introduction.js
-
CodeAlive/
-
Python.js
-
Features.js
-
-
Category/
-
PlayPython.js
-
PlayAI.js
-
-
ServiceCenter/
- FAQ.js
- QA.js
- Notice.js
-
IncContact/
- ContactPage.js
-
Common/
- store.js
- ScrollToTop.js
- Header.js
- Header2.js
- Header3.js
- Footer.js
- Editor.js
- Editor2.js
2. axios 사용 시, 주의사항
2-1. useEffect 에서 서버로부터 저장할 상태값을 다룰 때는, 반드시 방어적으로 axios를 호출해야 한다.
- 보수적으로 axios를 호출하지 않으면, 루핑이되거나 렌더링 횟수를 제대로 제어할 수 없으니 참고하도록 하자.
const axiosSample = () => {
// 조회할 데이터를 관리할 state
const [searchData, setSearchData] = useState("");
// ★중요 useEffect에서 저장할 상태값을 다룰 때는, 반드시 방어적으로 axios를 호출해야 함
useEffect(() => {
// 데이터가 있을 때에만, axios 호출
if (!bannerSearchData) {
axios
.get(SERVER_URL_SEARCH, {
headers: {
Authorization: TOKEN,
},
})
.then((response) => {
// 배너 목록 조회 데이터 변수에 값 저장
setBannerSearchData(response.data.items);
})
.catch((error) => {
console.log(error);
});
}, [searchData]); // 상태값은 dependencies에 추가
return(
// bannerSerachData가 true인지 검사
bannerSearchData &&
bannerSearchData.map((course, index) => {
...
});
);
}
2-2. jsx 안에서 axios 호출은 최대한 삼가도록 하자.
3. 로그인 후, MyPage 에 접근하게 하고 싶다면, 리덕스를 이용해 TOKEN을 관리하도록 하자.
[Login.js]
- 로그인에 성공하면, TOKEN을 Slice로 전송한다.
import { Input, Button, Image } from "antd";
import axios from "axios";
import MyPageSlice from "./MyPageSlice";
import { useDispatch } from 'react-redux';
const SERVER_URL = '서버 URL';
const Login = () => {
// 로그인 버튼 클릭시, 관리자 아이디인지 체크
const CheckLogin = () => {
axios
.post(SERVER_URL, {
userid: id,
password: pw,
})
.then((response) => {
// 슬라이스로 토큰 던지기
dispatch(MyPageSlice.actions.setTOKEN(response.data));
alert("환영합니다!");
navigate("/mypage");
})
.catch((error) => {
alert(error.response.data.messages[0]);
console.log(error.response.data.messages[0]);
});
};
return (
<Button onClick={CheckLogin}>로그인</Button>
);
}
export default Login;
[MyPageSlice.js]
- Login.js 에서 로그인 성공 시, 전달된 TOKEN을 저장하는 슬라이스.
// Admin의 토큰을 관리하는 슬라이스
import { createSlice } from "@reduxjs/toolkit";
const MyPageSlice = createSlice({
name: 'MyPageSlice',
initialState: {
TOKEN: "", // currCourseData
},
reducers: {
setTOKEN: (state, action) => {
let _TOKEN = 'Bearer ' + action.payload.accessToken
state.TOKEN = _TOKEN;
},
}
});
export default MyPageSlice;
[MyPage.js]
- 첫 페이지 진입 시, useEffect 에서 TOKEN이 있는 체크. TOKEN이 있다면, 렌더링하고 그렇지 않으면 경고를 띄우고 홈으로 이동한다.
import React, { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useSelector } from 'react-redux';
import MyPageSlice from './MyPageSlice';
const MyPage = () => {
// 토큰 가져오기
const TOKEN = useSelector((state) => {
return state.MyPageSlice.TOKEN;
});
useEffect(()=>{
// 토큰이 있는지 없는지 체크
if(!(TOKEN) || TOKEN.length <= 0) {
alert("로그인 후 이용해주세요.")
navigate("/");
}
return (
...
);
};
export default MyPage;
4. 전역으로 상태 관리를 하지 않을 거라면, 리덕스를 반드시 사용할 필요 없이 useState() 로 관리하도록 하자.
- 전역으로 상태를 관리해야 하는 경우.
- 토큰 정보.
- axios에서 조회할 핵심 데이터 덩어리.
5. 중요 ★) 다음 프론트 프로젝트에서 아래 순서로 셋팅하자.
순서 1. 기능명세 파악이 끝났다면, 리덕스 앱으로 프로젝트를 생성하자.
순서 2. 기능명세를 통해, 프로젝트 구조를 ‘라우트 기준’으로 폴더 > 파일 순으로 구성하자.
순서 3. 아래의 컴포넌트들은 재활용하도록 하자.
- 목록
- index.css
- index.js
- App.js
- ScrollToTop.js
- Editor.js
- VideoWrap.js
- store.js
- StoreSliceSmp.js
[index.css]
@import '~antd/dist/antd.css';
/* CSS 스타일 리셋 */
html, body, div, span, applet, object, iframe,
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
a, abbr, acronym, address, big, cite, code,
del, dfn, em, img, ins, kbd, q, s, samp,
small, strike, strong, sub, sup, tt, var,
b, u, i, center,
dl, dt, dd, ol, ul, li,
fieldset, form, label, legend,
table, caption, tbody, tfoot, thead, tr, th, td,
article, aside, canvas, details, embed,
figure, figcaption, footer, header, hgroup,
menu, nav, output, ruby, section, summary,
time, mark, audio, video {
margin: 0;
padding: 0;
border: 0;
font-size: 100%;
font: inherit;
vertical-align: baseline;
}
/* HTML5 display-role reset for older browsers */
article, aside, details, figcaption, figure,
footer, header, hgroup, menu, nav, section {
display: block;
}
body {
line-height: 1;
}
ol, ul {
list-style: none;
}
blockquote, q {
quotes: none;
}
blockquote:before, blockquote:after,
q:before, q:after {
content: '';
content: none;
}
table {
border-collapse: collapse;
border-spacing: 0;
}
body {
height: 3500px;
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}
[index.js]
import React from 'react';
import { createRoot } from 'react-dom/client';
import App from './App';
import { Provider } from 'react-redux';
import reportWebVitals from './reportWebVitals';
import { BrowserRouter } from 'react-router-dom';
import ScrollToTop from './app/ScrollToTop';
import store from './app/Common/store';
import './index.css';
const container = document.getElementById('root');
const root = createRoot(container);
// index.js 에서 라우터와 스토어, ScrollToTop을 구성
root.render(
<BrowserRouter>
<Provider store={store}>
<ScrollToTop />
<App />
</Provider>
</BrowserRouter>
);
reportWebVitals();
[App.js]
import React from 'react';
import { Routes, Route } from 'react-router-dom'
import MainPage from './app/MainPage';
import MyPage from './app/MyPage/MyPage';
import "antd/dist/antd.min.css";
function App() {
return (
<Routes>
<Route path="/" element={<MainPage />} />
<Route path="/mypage" element={<MyPage />} />
</Routes>
);
}
export default App;
[ScrollToTop.js]
import { useEffect } from "react";
import { useLocation } from "react-router-dom";
export default function ScrollToTop() {
const { pathname } = useLocation();
useEffect(() => {
window.scrollTo(0, 0);
}, [pathname]);
return null;
}
[Editor.js]
[VideoWrap.js]
import { useEffect, useRef } from "react";
import video from "../Assets/video.mp4";
const VideoWrap = () => {
return (
<div className='videoWrap'
style=>
{/* left는 영상 프레임 위로 올라와야 함 */}
<video
style=
autoPlay muted loop playsInline>
{/* <source src={VideoWrapSrc} type='video/mp4' /> */}
<source src={video} type='video/mp4'></source>
</video>
</div>
);
}
export default VideoWrap;
[store.js]
// configureStore <- storeSlice를 모으기 위한 거대한 스토어
import { configureStore } from '@reduxjs/toolkit';
import highLightBackColorSlice from './highLightBackColorSlice';
import headerSlice from './headerSlice';
import MyPageDashboardSlice from './MyPage/MyPageDashboardSlice';
// storeSlice들을 하나로 합칠 Store를 만들자
const store = configureStore({
// 여러 storeSlice 들을 하나의 거대한 reducer로 만들어줌
reducer: {
headerSlice: headerSlice.reducer,
MyPageDashboardSlice: MyPageDashboardSlice.reducer,
}
});
export default store;
샘플-[MyPageDashboardSlice.js]
// MyPageDashboard를 관리하는 학습이력 저장소
import { createSlice } from "@reduxjs/toolkit";
import { Image } from 'antd';
const MyPageDashboardSlice = createSlice({
name: 'MyPageDashboard',
initialState: {
data: { }, // currCourseData
key: '',
},
reducers: {
setData: (state, action) => {
state.data = action.payload;
},
setMyPageNavigationKey: (state, action) => {
state.key = action.payload;
},
}
});
export default MyPageDashboardSlice;
댓글남기기