프로젝트 리뷰 - 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;
 
      
댓글남기기