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;

6. 로딩 구현 시, 참고할 자료

[ReactLazy 링크]

댓글남기기