본문 바로가기
국비학원/수업기록

국비 지원 개발자 과정_Day82

by 루팽 2023. 3. 27.

<flux복습 - index.html>

<!DOCTYPE html>
<html lang="ko">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Flux Architecture</title>
  <script src="app.js" type="module"></script>
</head>
<body>
  <div id="count">count:</div>
  <div id="msg">msg:</div>
</body>
</html>

 

<flux복습 - app.js>

import { createStore } from "./redux.js"
import { reducer } from "./reducer.js" // worker함수
import { decrease, increase, setToastFalse, setToastMsg } from "./actions.js"

// 사용 - 함수 호출 -> store 생성하기 - 리액트에서는 index.js에서 할 일!
// -> index.js에서 모든 전역 state를 관리하기위해
// app.js에 있는 코드가 리액트 컴포넌트에 써야하는 코드임(store부분은 index.js)
// 문제제기 - app.js 하나에 모두 있을때는 파라미터에 reducer(구:worker)를 파라미터로 넘겨야 함
const store = createStore(reducer) // index.js

store.subscribe(function () { // 구독발행모델 - 함수호출
  // getState는 리액트에서 useSelector(state => state.userAUth) - 상태값을 store에서 읽어올 때 사용
  console.log(store.getState()) // 변경된 상태값 찍기 - 리액트 컴포넌트가 마운트될 때 찍기
  const state = store.getState()
  document.querySelector("#count").append(state.count)
  document.querySelector("#msg").append(state.msg)
})

 // dispatch가 리액트에선 const dispatch = useDispatch() -> dispatch(type, payload)
 // -> 액션을 스토어에 전달
 // 해당 컴포넌트에서 state값을 가져오기 - useSelector훅
store.dispatch(increase())
store.dispatch(increase())
store.dispatch(decrease())

store.dispatch(setToastMsg("관리자에게 문의하세요"))
store.dispatch(setToastFalse())

 

<flux복습 - redux.js>

// type을 정의하는 규칙 - 커링함수
// 매개변수 분할 처리
// 첫번째 파라미터 - 타입
// 두번째 파라미터 - 데이터를 받아오는 인자
// payload - 수하물
// 개발자가 정의한 data나 에러처리에 필요한 메시지값
// 요청에 대한 응답 메시지로 사용 가능 - Toast
// 실제 서비스에서는 필요없음 - react-redux
// 순서대로 처리할 필요가 있음 - 커링함수 패턴
export const actionCreator = (type) => (payload) => ({
  type,
  payload,
})

export const createStore = (reducer) => {
  let state;
  let handlers = []

  const dispatch = (action) => {
    console.log('dispatch 호출')
    state = reducer(state, action)
    handlers.forEach(handler => handler())
  }

  const subscribe = ((handler) => {
    handlers.push(handler)
  })

  const getState = () => {
    return state;
  }

  return {
    dispatch,
    getState,
    subscribe
  }
}

 

<flux복습 - reducer.js>

import * as ActionType from './action-type.js'
import { initializeState } from './state.js'

// store에서 관리해야하는 상태값의 종류가 점점 늘어남 - 객체 리터럴 - 열거형 연산자 - n개 관리, 초기화 처리
// 상태를 관리하는 변수 선언
// 선언된 변수들이 payload에 담김


//상태를 변형하는 것 - 리듀서
// 첫번째 파라미터 - 상태값
// 두번째 파라미터 - 액션 - dispatch를 통해 store에 전달
//action에 담긴 정보를 dispatch가 store에 전달하기 - flux architect
// One way방식 - 한 방향으로만 흐름
// action - dispatch - store - view
// action-type.js에 별도 정의
export const reducer = (state = initializeState, action) => { // 처음예제에선 worker
  switch(action.type){
    case ActionType.INCREASE:
      return {...state, count: state.count + 2}
      case ActionType.DECREASE:
      return {...state, count: state.count - 1}
      case ActionType.RESET:
      return { ...state, count: 0 };
      case ActionType.SET_MSG:
        // 깊은 복사에서 두번째 인자가 payload에 해당됨
        return {...state, status: action.bool, msg:action.msg}
      case ActionType.SET_FALSE:
        return {...state, status: action.bool, msg:action.msg}
    default:
      return {...state}
  }
}

 

<flux복습 - state.js>

export const initializeState = {
  count: 0,
  status: false, // Toast메시지 컴포넌트를 보여줄지 말지
  msg: "", // 사용자 요청에대한 처리 결과 메시지를 담기
} // 처음엔 let state로 처리함

 

<flux복습 - action.js>

import { DECREASE, INCREASE, RESET, SET_FALSE, SET_MSG } from "./action-type.js";
import { actionCreator } from "./redux.js";

// store.dispatch(increase()) - dispatch는 action을 store에 전달하는 허브
// store에 들어있는 상태값을 꺼내는 것이 getState - 리액트 useSelector

export const increase = actionCreator(INCREASE)
export const decrease = actionCreator(DECREASE)
export const reset = actionCreator(RESET)
export const setToastMsg = (msg) => {
  return{
    type: SET_MSG,
    msg: msg,
    bool: true,
  }
}

export const setToastFalse = () => {
  return{
    type: SET_FALSE,
    msg: '',
    bool: false,
  }
}

 

<flux복습 - action-type.js>

// 타입선언 - 대응되는건 함수
export const INCREASE = 'increase'
export const DECREASE = 'decrease'
export const RESET = 'reset'
export const SET_MSG = 'TOAST_STATUS/SET_MSG'
export const SET_FALSE= 'TOAST_STATUS/SET_FALSE'

 

<리액트 리덕스 - index.js>

import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
import { BrowserRouter } from "react-router-dom";
import "bootstrap/dist/css/bootstrap.min.css";
import "@fortawesome/fontawesome-free/js/all.js";
import ImageUploader from "./service/imageUploader";
import { Provider } from "react-redux";
import { legacy_createStore } from "redux";
import rootReducer from "./redux/rootReducer";
import AuthLogic from "./service/authLogic";
import firebaseApp from "./service/firebase";
import { setAuth } from "./redux/userAuth/action";

// 리덕스 적용하기
const store = legacy_createStore(rootReducer)
// AuthLogic객체 생성하기
const authLogic = new AuthLogic(firebaseApp)
// store에 있는 초기 상태정보 출력하기
store.dispatch(setAuth(authLogic.getUserAuth(), authLogic.getGoogleAuthProvider()))
console.log(store.getState());
// 이미지 업로더 객체 생성
const imageUploader = new ImageUploader();
const root = ReactDOM.createRoot(document.getElementById("root"));
// 리덕스 추가 - store 생성
// createStore 호출
root.render(
  <>
    <Provider store={store}>
      <BrowserRouter>
        <App imageUploader={imageUploader} />
      </BrowserRouter>
    </Provider>
  </>
);

 

<리액트 리덕스 - App.jsx>

import { Route, Routes } from 'react-router-dom';
import LoginPage from './components/auth/LoginPage';
import KakaoRedirectHandler from './components/kakao/KakaoRedirectHandler';
import Profile from './components/kakao/Profile';
import MemberPage from './components/page/MemberPage';
import HomePage from './components/page/HomePage';
import DeptPage from './components/page/DeptPage';
import DeptDetail from './components/dept/DeptDetail';
import RepleBoardPage from './components/page/RepleBoardPage';
import Signup from './components/member/Signup';
import Toast from './components/Toast';
import { useDispatch, useSelector } from 'react-redux';
import { useEffect } from 'react';
import { setToastMsg } from './redux/toastStatus/action';

function App({imageUploader}) {
  const dispatch = useDispatch()
  const toastStatus = useSelector(state => state.toastStatus)
  useEffect(() => {
    dispatch(setToastMsg('회원가입을 해주세요.'))
  }, [])

  return (
    <>
      <div style={{height: '100vh'}}>
        {toastStatus.status && <Toast />}
        <Routes>
          <Route path='/login' exact={true} element={<LoginPage />} />
          <Route path='/' exact={true} element={<HomePage />} />
          <Route path='/home' exact={true} element={<HomePage />} />
          <Route path='/member/signup' exact={true} element={<Signup />} />
          <Route path='/repleboard' element={<RepleBoardPage />} />
          <Route path='/dept/:gubun' element={<DeptPage imageUploader={imageUploader} />} />
          {/* 컴포넌트 함수를 호출하는 것이다 - 마운트(화면이 보여지는 것) - return이 호출되었다 */}
          <Route path='/deptdetail/:deptno' element={<DeptDetail imageUploader={imageUploader} />} />
          <Route path='/auth/kakao/callback' exact={true} element={<KakaoRedirectHandler />} />
          <Route path="/member" exact={true} element={<MemberPage imageUploader={imageUploader} />} />
          <Route path="/profile" exact={true} element={<Profile />} />
        </Routes>
      </div>
    </>
  );
}

export default App;

 

<리액트 리덕스 - rootReducer.js>

import { combineReducers } from "redux"
import userAuth from "./userAuth/reducer";
import toastStatus from "./toastStatus/reducer";

const rootReducer = combineReducers({
  userAuth,
  toastStatus,
})

export default rootReducer;

 

<리액트 리덕스 - reducer.js>

import { toastStatus } from "./state";
import { SET_FALSE, SET_MSG } from "./action";

// 아래 toastInfo함수이름을 직접 사용하지 않음
export default function toastInfo(state = toastStatus, action) {
  switch(action.type) {
    case SET_MSG:
      return {
        ...state, status: action.bool, msg: action.msg
      }
    case SET_FALSE:
      return {
        ...state, status: action.bool, msg: action.msg
      }
    default:
      return { ...state }
  }
} // end of toastInfo

 

<리액트 리덕스 - state.js>

// reducer에서 변경하는 data에대한 선언 및 초기화
export const toastStatus = {
  status: false,
  msg: "",
}

 

<리액트 리덕스 - action.js>

// action에서 사용되는 타입 선언
export const SET_MSG = 'TOAST_STATUS/SET_MSG'
export const SET_FALSE = 'TOAST_STATUS/SET_FALSE'

// Action을 dispatch를 통해서 store에 전달할 때 호출되는 함수
// 이것이 리듀서에 전달되면 switch문에서 변화
export const setToastMsg = (msg) => {
  return {
    type: SET_MSG,
    msg: msg,
    bool: true,
  }
}

export const setToastFalse = () => {
  return {
    type: SET_FALSE,
    msg: '',
    bool: false,
  }
}

 

<리액트 리덕스 - reducer.js>

import { SET_AUTH } from "./action"
import { userAuth } from "./state"

export default function userInfo(state = userAuth, action){
  switch(action.type){
    case SET_AUTH:
      return{
        ...state,
        auth: action.auth,
        googleProvider: action.googleProvider,
      }
    default:
      return { ...state }
  }
}

 

<리액트 리덕스 - state.js>

// service > authLogic.js > AuthLogic객체선언
export const userAuth = {
  auth: "", // firebase/auth
  googleProvider: "", // 구글 공급자 정보
  // githubProvider:"", 깃헞브 공급자 정보
}

 

<리액트 리덕스 - action.js>

export const SET_AUTH = "USER_AUTH/SET_AUTH"

export const setAuth = (auth, googleProvider) => {
  return {
    type: SET_AUTH,
    auth: auth,
    googleProvider: googleProvider,
  }
}

 

<리액트 리덕스 - authLogic.js>

import { getAuth, GoogleAuthProvider, signInWithPopup } from 'firebase/auth'

class AuthLogic {
  constructor() {
    this.auth = getAuth()
    this.googleProvider = new GoogleAuthProvider()
  }
  getUserAuth = () => {
    return this.auth
  }
  getGoogleAuthProvider = () => {
    return this.googleProvider
  }
}
export default AuthLogic

export const onAuthChange = (auth) => {
  return new Promise((resolve) => {
    // 사용자가 바뀌었을 때 콜백함수를 받아서
    auth.onAuthStateChanged((user) => {
    resolve(user)
    });
  }) // end of Promise
} // end of onAuthChange

// 로그아웃 버튼 클릭시 호출하기
export const logout = (auth) => {
  return new Promise((resolve, reject) => {
    auth.signOut().catch(e => reject(e + '로그아웃 오류입니다.'))
    // 로그인 성공시 세션 스토리지에 담아둔 정보를 모두 지운다
    sessionStorage.clear();
    // 서비스를 더 이상 사용하지 않는 경우이므로 돌려줄 값은 없다
    resolve();
  })
} // end of logout

 // 로그인 시도시 구글인증인지 아니면 깃허브 인증인지 문자열로 넘겨받음
  // 구글 인증인 경우 - Google
  // 깃허브 인증인 경우 - Github
  export const loginGoogle = (auth, googleProvider) => {
    return new Promise((resolve, reject) => {
      signInWithPopup(auth, googleProvider).then(
        (result) => {
          const user = result.user; // 구글에 등록되어있는 profile정보가 담겨있음
          console.log(user);
          resolve(user)
        }
      ).catch(e => reject(e))
    })
  }

 

<리액트 리덕스 - Toast.jsx>

import React, { useEffect } from 'react'
import { useDispatch, useSelector } from 'react-redux';
import styled from 'styled-components';
import { setToastFalse } from '../redux/toastStatus/action';
import './toast.css';

const ToastDiv = styled.div`
  position: fixed;
  top: 50%;
  left: 50%;
  padding: 11px;
  min-width: 350px;
  transform: translate(-50%, -50%);
  justify-content: center;
  text-align: center;
  //font-weight: bold;
  font-size: 18px;
  z-index: 99;
  background: rgba(0, 0, 0, 0.7);
  color: #ffffff;
  border-radius: 4px;
  border: 1px solid #000000;
`

const Toast = () => {

  const toastStatus = useSelector(state => state.toastStatus);
  const dispatch = useDispatch();

  
  useEffect(() => {
    if (toastStatus.status) {
      setTimeout(() => {
        dispatch(setToastFalse()); 
      }, 1500)
    }

  }, [toastStatus.status, dispatch]);
  

  return (
    <ToastDiv>{JSON.stringify(toastStatus.msg)}</ToastDiv>
  );
};

export default Toast;

 

<리액트 리덕스 - LoginPage.jsx>

import React from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { Link, useNavigate } from 'react-router-dom'
import { setToastMsg } from '../../redux/toastStatus/action'
import { loginGoogle } from '../../service/authLogic'
import './loginpage.css'

const LoginPage = () => {
  const navigate = useNavigate()
  // One-Way 바인딩에서 액션을 스토어에 전달할 때 디스패치 사용함
  const dispatch = useDispatch()
  // store에 초기화된 state 안에 담긴 data 꺼내올 때 셀렉터 사용
  const userAuth = useSelector((store) => store.userAuth)
  const auth = useSelector((store) => store.userAuth.auth)
  console.log(auth);
  const googleProvider = useSelector((store) => store.userAuth.googleProvider)
  console.log(googleProvider);

  const handleGoogle = async(event) => {
    event.preventDefault()
    try {
      const result = await loginGoogle(auth, googleProvider)
      console.log(result.uid);
      window.sessionStorage.setItem('userId', result.uid)
      navigate('/')
    } catch (error) {
      dispatch(setToastMsg(error + ': 로그인 오류입니다.')) // [object, object] -> JSON.stringify()
    }
  } //end of handleGoogle

  return (
    <>
    <div className="bg"></div>
    <form className="login__form" action="">
      <h1>로그인</h1>
      <p>신규 사용자이신가요? <Link to="#">계정 만들기</Link></p>
      <label htmlFor="useremail">이메일 주소
        <input type="email" id="useremail" placeholder="이메일 주소를 입력해주세요."/>
      </label>
      <input type="submit" className="btn__submit" value="계속" />
      <div className="divider">
        <hr />
        <span>또는</span>
      </div>
      <button className="btn__google" onClick={handleGoogle}>
        <i className="icon icon-google"></i>Google으로 계속
      </button>
      <button className="btn__facebook">
        <i className="icon icon-facebook"></i>Facebook으로 계속
      </button>
      <p className="captcha__text">
        reCAPTCHA로 보호되며 Google <Link to="#">개인정보보호 정책</Link> 및
        <Link to="#">서비스 약관</Link>의 적용을 받습니다.
      </p>
      </form>
    </>
  )
}

export default LoginPage

 

<리액트 리덕스 - HomePage.jsx>

import React from 'react'
import { Button } from 'react-bootstrap'
import { useNavigate } from 'react-router-dom'
import BlogHeader from '../include/BlogHeader'
import KakaoMap from '../kakao/KakaoMap'
import {ContainerDiv, FormDiv, HeaderDiv} from '../styles/FormStyle'

const HomePage = () => {
  const navigate = useNavigate()
  const handleLogin = () => {
    console.log('로그인 요청');
    navigate('/login')
  }

  return (
    <>
      <ContainerDiv>
        <BlogHeader />
        <HeaderDiv>
          <h1 style={{marginLeft:"10px"}}>터짐블로그</h1>
          <Button onClick={handleLogin}>로그인</Button>
        </HeaderDiv>
        <FormDiv>
          <div>이벤트존</div>
          <hr style={{height:"2px"}} />
          <div>추천 수업존</div>
          <hr style={{height:"2px"}} />
          <div><KakaoMap /></div>
          <div>카카오맵존</div>
          <hr style={{height:"2px"}} />
        </FormDiv>
      </ContainerDiv>
    </>
  )
}

export default HomePage

 

<props로 넘겨받기 - index.js>

import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import App from "./App";
import ReduxApp from "../ReduxApp";

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
  <>
    <App />
  </>
);

 

<props로 넘겨받기 - App.jsx>

import "./App.css";
import Header from "./components/include/Header";
import Footer from "./components/include/Footer";
import MainPage from "./components/page/MainPage";
import { useState } from "react";

function App() {
  const num = 0; // 리렌더링 없음
  const [number, setNumber] = useState(0) // 리렌더링O
  const addNumber = () => {
    console.log('addNumber');
    setNumber(number + 1)
  }

  return (
    <>
      <div className="container">
        <Header number={number} />
        <MainPage number={number} addNumber={addNumber} />
        <Footer addNumber={addNumber} />
        <button onClick={addNumber}>추가-{number}</button>
      </div>
    </>
  );
}

export default App;

/*
리렌더링 3가지 - 부모컴포넌트, props, state
*/

 

<props로 넘겨받기 - Header.jsx>

import React from 'react'

const Header = ({number}) => { // props로 넘어온 값을 바로 구조분해할당
  return (
    <>
      <div className='header_container'>
        헤더영역
        <h4>{number}</h4>
      </div>
    </>
  )
}

export default Header

 

<props로 넘겨받기 - MainPage.jsx>

import React from 'react'

const MainPage = (props) => {
  const{number, addNumber} = props
  return (
    <>
      <div className='main_container'>
        컨텐츠 영역
        <h4>{number}</h4>
        <button onClick={addNumber}>메인버튼</button>
      </div>
    </>
  )
}

export default MainPage

 

<props로 넘겨받기 - Footer.jsx>

import React from 'react'

const Footer = (props) => {
  const {addNumber} = props // 구조분해할당
  return (
    <>
      <div className='footer_container'>
        푸터영역<br />
        <button onClick={addNumber}>푸터증가버튼</button>
      </div>
    </>
  )
}

export default Footer

 

<Redux로 바꾸기 - index.js>

import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import App from "./App";
import ReduxApp from "./ReduxApp";
import { Provider } from "react-redux";
import { legacy_createStore } from "redux";
import reducer from "./redux/store";

const store = legacy_createStore(reducer)
console.log(store.getState());
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
  <>
    {/* <App /> */}
    <Provider store={store}>
      <ReduxApp />
    </Provider>
  </>
);

 

<Redux로 바꾸기 - ReduxApp.jsx>

import React from 'react'
import ReduxFooter from './components/include/ReduxFooter'
import ReduxHeader from './components/include/ReduxHeader'
import ReduxMainPage from './components/page/ReduxMainPage'

const ReduxApp = () => {
  return (
    <>
      <h3>리덕스 적용 예제</h3>
      <div className='container'>
        <ReduxHeader />
        <ReduxMainPage />
        <ReduxFooter />
      </div>
    </>
  )
}

export default ReduxApp

 

<Redux로 바꾸기 - ReduxHeader.jsx>

import React from 'react'

const ReduxHeader = () => {
  return (
    <>
      <div className='header_container'>
        리덕스 헤더영역
      </div>
    </>
  )
}

export default ReduxHeader

 

<Redux로 바꾸기 - ReduxMainPage.jsx>

import React from 'react'
import { useSelector } from 'react-redux'

const ReduxMainPage = () => {
  const number = useSelector(store => store.number)
  return (
    <>
      <div className='main_container'>
        컨텐츠 영역
        <h4>{number}</h4>
      </div>
    </>
  )
}

export default ReduxMainPage

 

<Redux로 바꾸기 - ReduxFooter.jsx>

import React, { useEffect } from 'react'
import { useDispatch } from 'react-redux'
import { increase } from '../../redux/store'

const ReduxFooter = () => {
  const dispatch = useDispatch()
  const handleAdd = (e) => {
    e.preventDefault() // 이벤트 버블링 차단
    dispatch(increase('가나다'))
  }

  return (
    <>
      <div className='footer_container'>
        리덕스 푸터영역<br />
        <button onClick={handleAdd}>증가버튼</button>
      </div>
    </>
  )
}

export default ReduxFooter

 

<Redux로 바꾸기 - store.js>

// 액션
export const increase = (mem_name) => ({
  type: 'INCREASE',
  payload: mem_name,
})

// 초기상태 만들기
const initstate = {
  number: 0,
  mem_name: '이순신',
  empVO: {empno: 7566, ename: '나신입'},
  depts: [
    { DEPTNO: 10, DNAME: '총무부', LOC: '인천'},
    { DEPTNO: 20, DNAME: '영업부', LOC: '서울'},
    { DEPTNO: 30, DNAME: '개발부', LOC: '괌'},
  ],
  auth: '',
  googleProvider: '',
}

// worker가 reducer함수로 전환
//액션의 결과를 필터링 - 리듀서
const reducer = (state = initstate, action) => {
  switch(action.type) {
    case "INCREASE":
      // return이되면 그걸 호출한 쪽에서 받는게 아니고, return되는 순간 바로 화면이 변경됨
      return { ...state, number: state.number + 1, mem_name: action.payload }
    default:
      return { ...state } // 깊은 복사, 새로운 객체 생성됨
  }
}
export default reducer

댓글