참고
- 리덕스로 상태 관리하기
위의 링크를 참고해서 구현하면 로그인 정보를 유지하기가 매우 까다롭다.
App.js에서 loginStatus를 useState로 관리하고, 각 Component에 전달해야 하기 때문이다.
리덕스를 이용하여 아래와 같이 로그인 상태 정보를 유지해 보자.
아래의 구현에서부터 시작한다.
Node JS
githubLogin.js
const axios = require("axios");
const express = require("express");
const router = express.Router();
const dotenv = require('dotenv');
const getResponse = async (code, clientID) => {
dotenv.config();
const response = await axios.post(
"https://github.com/login/oauth/access_token",
{
code,
client_id: clientID,
client_secret: process.env.CLIENT_SECRET,
},
{
headers: {
accept: "application/json",
},
}
);
return response;
};
router.get("/", async (req, res) => {
let code = req.query.code;
let clientID = req.query.clientID;
let response = await getResponse(code, clientID);
console.log(response.data.access_token);
res.send({token: response.data.access_token});
});
module.exports = router;
리액트
App.js
import React, { useState } from "react";
import { Route, Link, Routes } from "react-router-dom";
import "./App.css";
import Router1 from "./page/Router1.js";
import Router2 from "./page/Router2.js";
import GitHubLoginCallback from "./page/GitHubLoginCallback.js";
const App = () => {
const [loginStatus, setLoginStatus] = useState(false);
return (
<div className="App">
<div className="router">
<span>
<Link to="/r1">Router 1</Link>
</span>
<span>
<Link to="/r2">Router 2</Link>
</span>
</div>
<div>
<Routes>
<Route
path="/r1"
element={
<Router1
loginStatus={loginStatus}
setLoginStatus={setLoginStatus}
/>
}
/>
<Route
path="/r2"
element={
<Router2
loginStatus={loginStatus}
setLoginStatus={setLoginStatus}
/>
}
/>
<Route
path="/callback"
element={
<GitHubLoginCallback
loginStatus={loginStatus}
setLoginStatus={setLoginStatus}
/>
}
/>
</Routes>
</div>
</div>
);
};
export default App;
GitHubLoginCallback.js
import React, { useEffect } from "react";
import { useNavigate } from "react-router-dom";
import axios from "axios";
import queryString from "query-string";
import * as ck from "../cookielibrary.js";
const clientID = process.env.REACT_APP_CLIENT_ID;
const GitHubLoginCallback = ({ loginStatus, setLoginStatus }) => {
const navigate = useNavigate();
const getAccessToken = async (code) => {
try {
let server = `http://192.168.55.120:3002`;
let accessInfo = await axios.get(
`${server}/githubLogin?code=${code}&clientID=${clientID}`
);
let token = accessInfo.data.token;
const userResponse = await axios.get("https://api.github.com/user", {
headers: {
Authorization: `Bearer ${token}`,
},
});
const userData = userResponse.data;
const avatarUrl = userData.avatar_url;
const loginID = userData.login;
console.log(userData);
console.log(avatarUrl);
console.log(loginID);
let options = {
path: "/",
maxAge: 60 * 60 * 24 * 7, // seconds
// secure: true, // https 연결에만 전송
// httpOnly: true, // 클라이언트에서 read 불가
};
ck.setCookies("GITHUB_TOKEN", token, options);
ck.setCookies("AVATAR_URL", avatarUrl, options);
ck.setCookies("LOGIN_ID", loginID, options);
setLoginStatus(true);
navigate('/', { replace: true });
} catch (e) {
console.log(e);
setLoginStatus(false);
}
};
const getCode = () => {
let qs = queryString.parse(window.location.search);
let code = qs.code;
getAccessToken(code);
};
useEffect(() => getCode(), []);
return <div>{loginStatus ? "로그인 성공!" : "로그인 실패..."}</div>;
};
export default GitHubLoginCallback;
GitHubLoginButton.js
import React from "react";
import Button from "@mui/material/Button";
import Box from "@mui/material/Box";
import Stack from "@mui/material/Stack";
import Avatar from "@mui/material/Avatar";
const clientID = process.env.REACT_APP_CLIENT_ID;
const callbackURL = "http://localhost:3000/callback";
const loginURL = `https://github.com/login/oauth/authorize?client_id=${clientID}&scope=repo:status read:repo_hook user:email&redirect_uri=${callbackURL}`;
const GitHubLoginButton = ({ loginStatus, setLoginStatus }) => {
const logout = () => {
setLoginStatus(false);
localStorage.removeItem("GITHUB_TOKEN");
localStorage.removeItem("AVATAR_URL");
localStorage.removeItem("LOGIN_ID");
};
return (
<div>
<Box sx={{ m: 2 }}>
{loginStatus === false && (
<div>
<Stack direction="row" spacing={2}>
<Avatar alt="GitHub Login" src="/static/images/avatar/1.jpg" />
<Button variant="outlined" color="secondary" href={loginURL}>
로그인
</Button>
</Stack>
</div>
)}
{loginStatus === true && (
<div>
<Stack direction="row" spacing={2}>
<Avatar
alt="GitHub Login"
src={localStorage.getItem("AVATAR_URL")}
/>
<Button variant="outlined" color="error" onClick={logout}>
로그아웃
</Button>
</Stack>
</div>
)}
</Box>
</div>
);
};
export default GitHubLoginButton;
Router1.js
import React from "react";
import GitHubLoginButton from "./GitHubLoginButton";
const Router1 = ({ loginStatus, setLoginStatus }) => {
return (
<div>
<p>Router 1</p>
<GitHubLoginButton
loginStatus={loginStatus}
setLoginStatus={setLoginStatus}
/>
</div>
);
};
export default Router1;
Router2.js
import React from "react";
import GitHubLoginButton from "./GitHubLoginButton";
const Router2= ({ loginStatus, setLoginStatus }) => {
return (
<div>
<p>Router 2</p>
<GitHubLoginButton
loginStatus={loginStatus}
setLoginStatus={setLoginStatus}
/>
</div>
);
};
export default Router2;
리덕스 구현
리덕스를 설치하자.
$ npm install redux react-redux --legacy-peer-deps
먼저 상태를 저장할 store를 등록해야 한다.
src/store/store.js
import { legacy_createStore as createStore } from "redux";
import rootReducer from "../reducer/rootReducer";
const store = createStore(rootReducer);
export default store;
위의 스토어에 저장될 루트는 다음과 같다.
src/reducer/rootReducer.js
import loginStatus from "./loginReducers";
import { combineReducers } from "redux";
const rootReducer = combineReducers({ loginStatus });
export default rootReducer;
루트에 저장되어 있는 loginStatus 리듀서는 다음과 같다. (리듀서가 추가되면 combineReducers에서 합치면 된다.)
src/reducer/loginReducers.js
const loginStatus = (state = {}, action) => {
switch (action.type) {
case "LOGIN":
return { ...state, loginInfo: action.loginInfo, status: true };
case "LOGOUT":
return { ...state, loginInfo: "", status: undefined };
default:
return state;
}
};
export default loginStatus;
리듀서의 상태를 변화시킬 액션은 다음과 같다.
src/actions/loginAction.js
export const login = (loginInfo) => {
return {
type: "LOGIN",
loginInfo,
};
};
export const logout = () => {
return {
type: "LOGOUT",
};
};
Component에 리덕스 적용
먼저 최상단에 LoginStatus Component를 실행시켜서 로그인 상태를 확인하자.
여기서는 로컬 스토리지에 값이 있다면 로그인을 했다고 판단한다.
그리고 스토리지에 값이 있으므로, dispatch로 스토어에 login 정보를 추가한다.
src/LoginStatus.js
import { useEffect } from "react";
import { useDispatch } from "react-redux";
import * as actions from "./actions/loginAction";
const LoginStatus = () => {
const dispatch = useDispatch();
useEffect(() => {
let check = localStorage.getItem("LOGIN_INFO");
if (check === null) return;
let loginInfo = JSON.parse(check);
console.log({ loginInfo });
dispatch(actions.login(loginInfo));
}, []);
return null;
};
export default LoginStatus;
위의 컴포넌트를 index.js에서 <App /> 보다 위에 추가하면,
로그인 상태를 확인한 후, App이 실행된다.
//index.js
import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import App from "./App";
import reportWebVitals from "./reportWebVitals";
import { BrowserRouter } from "react-router-dom";
import LoginStatus from "./LoginStatus"; // 로그인 확인
import { Provider } from "react-redux"; // redux 추가
import store from "./store/store";
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<BrowserRouter basename={process.env.PUBLIC_URL}>
<Provider store={store}>
<LoginStatus />
<App />
</Provider>
</BrowserRouter>
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
GitHubLoginCallback.js에서 getAccessToken 메서드를 수정하자.
필요한 로그인 정보는 action에 등록된 login 액션을 이용하면 된다.
그리고 로그인 정보를 로컬 스토리지에 저장하자. (필요에 따라 쿠키나 세션 스토리지 사용)
import { useDispatch } from "react-redux";
import * as actions from "../actions/loginAction";
const GitHubLoginCallback = () => {
const dispatch = useDispatch();
const getAccessToken = async (code) => {
try {
...
const userData = userResponse.data;
const avatarUrl = userData.avatar_url;
const loginID = userData.login;
let loginInfo = {
avatarUrl, loginID
}
// 로그인 처리
dispatch(actions.login(loginInfo));
localStorage.setItem("LOGIN_INFO", JSON.stringify(loginInfo));
navigate('/', { replace: true });
} catch (e) {
console.log("error ", e);
}
};
...
로그인 버튼(GitHubLoginButton.js)은 기존의 useState로 관리했던 loginStatus가 필요가 없으므로 삭제한다.
리덕스를 이용하여 loginStatus를 확인해서 status 상태에 따라 로그인 / 로그아웃 버튼을 렌더링한다.
로그아웃은 로컬 스토리지를 삭제하고 dispatch로 logout 액션을 호출하면 된다.
import React from "react";
import { useSelector, useDispatch } from "react-redux";
import * as actions from "../actions/loginAction";
...
const GitHubLoginButton = () => {
const loginStatus = useSelector((state) => state.loginStatus);
const dispatch = useDispatch();
const logout = () => {
localStorage.removeItem("LOGIN_INFO");
dispatch(actions.logout());
};
return (
<div>
<Box sx={{ m: 2 }}>
{loginStatus.status === undefined && (
<div>
<Stack direction="row" spacing={2}>
<Avatar alt="GitHub Login" src="/static/images/avatar/1.jpg" />
<Button variant="outlined" color="secondary" href={loginURL}>
로그인
</Button>
</Stack>
</div>
)}
{loginStatus.status === true && (
<div>
<Stack direction="row" spacing={2}>
<Avatar
alt="GitHub Login"
src={localStorage.getItem("AVATAR_URL")}
/>
<Button variant="outlined" color="error" onClick={logout}>
로그아웃
</Button>
</Stack>
</div>
)}
</Box>
</div>
);
};
export default GitHubLoginButton;
최종적으로 Router1, 2의 loginStatus는 필요가 없으므로 아래와 같이 깔끔하게 코드가 변하게 된다.
즉, 원하는 컴포넌트에서 useSelector를 이용하여 로그인 상태를 알 수 있게 되었다.
import React from "react";
import GitHubLoginButton from "./GitHubLoginButton";
const Router1= () => {
return (
<div>
<p>Router 1</p>
<GitHubLoginButton/>
</div>
);
};
export default Router1;
이제 로그인 정보가 Component 내에서 리덕스로 공유되고, 새로고침을 해도 상태가 유지된다.
나머지는 전체 코드를 참고하자.
src/store/store.js
import { legacy_createStore as createStore } from "redux";
import rootReducer from "../reducer/rootReducer";
const store = createStore(rootReducer);
export default store;
src/reducer/rootReducer.js
import loginStatus from "./loginReducers";
import { combineReducers } from "redux";
const rootReducer = combineReducers({ loginStatus });
export default rootReducer;
src/reducer/loginReducers.js
const loginStatus = (state = {}, action) => {
switch (action.type) {
case "LOGIN":
return { ...state, loginInfo: action.loginInfo, status: true };
case "LOGOUT":
return { ...state, loginInfo: "", status: undefined };
default:
return state;
}
};
export default loginStatus;
src/actions/loginAction.js
export const login = (loginInfo) => {
return {
type: "LOGIN",
loginInfo,
};
};
export const logout = () => {
return {
type: "LOGOUT",
};
};
src/LoginStatus.js
import { useEffect } from "react";
import { useDispatch } from "react-redux";
import * as actions from "./actions/loginAction";
const LoginStatus = () => {
const dispatch = useDispatch();
useEffect(() => {
let check = localStorage.getItem("LOGIN_INFO");
if (check === null) return;
let loginInfo = JSON.parse(check);
console.log({ loginInfo });
dispatch(actions.login(loginInfo));
}, []);
return null;
};
export default LoginStatus;
index.js
//index.js
import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import App from "./App";
import reportWebVitals from "./reportWebVitals";
import { BrowserRouter } from "react-router-dom";
import LoginStatus from "./LoginStatus";
import { Provider } from "react-redux";
import store from "./store/store";
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<BrowserRouter basename={process.env.PUBLIC_URL}>
<Provider store={store}>
<LoginStatus />
<App />
</Provider>
</BrowserRouter>
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
GitHubLoginCallback.js
import React, { useEffect } from "react";
import { useNavigate } from "react-router-dom";
import { useDispatch } from "react-redux";
import * as actions from "../actions/loginAction";
import axios from "axios";
import queryString from "query-string";
const clientID = process.env.REACT_APP_CLIENT_ID;
const GitHubLoginCallback = () => {
const navigate = useNavigate();
const dispatch = useDispatch();
const getAccessToken = async (code) => {
try {
let server = `http://192.168.55.120:3002`;
let accessInfo = await axios.get(
`${server}/githubLogin?code=${code}&clientID=${clientID}`
);
let token = accessInfo.data.token;
const userResponse = await axios.get("https://api.github.com/user", {
headers: {
Authorization: `Bearer ${token}`,
},
});
const userData = userResponse.data;
const avatarUrl = userData.avatar_url;
const loginID = userData.login;
console.log(userData);
console.log(avatarUrl);
console.log(loginID);
let loginInfo = {
avatarUrl, loginID
}
dispatch(actions.login(loginInfo));
localStorage.setItem("LOGIN_INFO", JSON.stringify(loginInfo));
navigate('/', { replace: true });
} catch (e) {
console.log("error ", e);
}
};
const getCode = () => {
let qs = queryString.parse(window.location.search);
let code = qs.code;
getAccessToken(code);
};
useEffect(() => getCode(), []);
return <div>{"로그인 시도 중..."}</div>;
};
export default GitHubLoginCallback;
GitHubLoginButton.js
import React from "react";
import { useSelector, useDispatch } from "react-redux";
import * as actions from "../actions/loginAction";
import Button from "@mui/material/Button";
import Box from "@mui/material/Box";
import Stack from "@mui/material/Stack";
import Avatar from "@mui/material/Avatar";
const clientID = process.env.REACT_APP_CLIENT_ID;
const callbackURL = "http://localhost:3000/callback";
const loginURL = `https://github.com/login/oauth/authorize?client_id=${clientID}&scope=repo:status read:repo_hook user:email&redirect_uri=${callbackURL}`;
const GitHubLoginButton = () => {
const loginStatus = useSelector((state) => state.loginStatus);
const dispatch = useDispatch();
const logout = () => {
localStorage.removeItem("LOGIN_INFO");
dispatch(actions.logout());
};
return (
<div>
<Box sx={{ m: 2 }}>
{loginStatus.status === undefined && (
<div>
<Stack direction="row" spacing={2}>
<Avatar alt="GitHub Login" src="/static/images/avatar/1.jpg" />
<Button variant="outlined" color="secondary" href={loginURL}>
로그인
</Button>
</Stack>
</div>
)}
{loginStatus.status === true && (
<div>
<Stack direction="row" spacing={2}>
<Avatar
alt="GitHub Login"
src={localStorage.getItem("AVATAR_URL")}
/>
<Button variant="outlined" color="error" onClick={logout}>
로그아웃
</Button>
</Stack>
</div>
)}
</Box>
</div>
);
};
export default GitHubLoginButton;
Router1.js
import React from "react";
import GitHubLoginButton from "./GitHubLoginButton";
const Router1= () => {
return (
<div>
<p>Router 1</p>
<GitHubLoginButton/>
</div>
);
};
export default Router1;
Router2.js
import React from "react";
import GitHubLoginButton from "./GitHubLoginButton";
const Router2 = () => {
return (
<div>
<p>Router 2</p>
<GitHubLoginButton />
</div>
);
};
export default Router2;
'개발 > React' 카테고리의 다른 글
리액트 - html2pdf로 PDF 다운로드하기 (0) | 2024.01.28 |
---|---|
React Material - Stepper로 워크 플로우 관리하기 (Managing Workflows with Mui Stepper) (0) | 2024.01.24 |
리액트 - Toast UI 에디터로 이미지를 포함한 깃허브 마크다운 저장하기 (Toast UI Markdown Editor) (0) | 2024.01.17 |
리액트 - 캡처한 이미지 여러 개 업로드하기 (Upload Captured Images to GitHub) (0) | 2024.01.17 |
리액트 - 캡처한 이미지를 깃허브에 업로드하기 (Upload Captured Image to GitHub) (0) | 2024.01.17 |
댓글