이번 게시글에서는 git의 OAuth를 연동하여 사용자의 정보를 불러오는 기능을 구현해보려 한다.
CRA(Create React App) 명령어를 통해 react app을 만들어 준 상태임을 가정한다.
(yarn add react-redux redux-devtools-extenstion react-router-dom styled-components mdi-react axios도 실행)
1. OAuth 계정 생성
아래 사진의 경로를 따라 이동한다.
위 과정을 모두 마치면 아래와 같은 화면을 확인할 수 있다.
- Application name/description은 자유롭게 작성.
- Homepage URL과 Authorization callback URL은 각각 http://localhost:3000, http://localhost:3000/login으로 입력
* Authorizaion callback은 react app에서 최초로 git OAuth에 접속하여 code 정보를 얻은 다음 redirect할 경로로 사용된다.
위 순서를 통해 얻은 client id, secret 정보는 project의 root dir에 .env파일을 생성하여 아래와 같이 저장한다.( src 하위 경로 아님 주의 )
REACT_APP_CLIENT_ID=YOUR_CLIENT_ID
REACT_APP_REDIRECT_URI=http://localhost:3000/login
REACT_APP_CLIENT_SECRET=YOUR_SECRET_KEY
REACT_APP_PROXY_URL=http://localhost:5001/
- proxy_url이 무엇인지는 나중에 알아보자
2. redux 이용해 login state생성
src 폴더 아래에 modules 폴더를 만들고 login.js 파일을 생성하여 아래 내용을 붙여넣는다.
/* 액션 타입 만들기 */
const LOGIN = 'LOGIN';
const LOGOUT = 'LOGOUT';
/* 액션 생성함수 만들기 */
// 액션 생성함수를 만들고 export 키워드를 사용해서 내보내주세요.
export const actionLogin = payload => ({ type: LOGIN , payload })
export const actionLogout = () => ({ type: LOGOUT })
const initialState = {
user: JSON.parse(localStorage.getItem("user")) || null,
isLoggedIn: JSON.parse(localStorage.getItem("isLoggedIn")) || false,
client_id: process.env.REACT_APP_CLIENT_ID,
redirect_uri: process.env.REACT_APP_REDIRECT_URI,
client_secret: process.env.REACT_APP_CLIENT_SECRET,
proxy_url: process.env.REACT_APP_PROXY_URL
};
export default function login (state = initialState, action) {
switch (action.type) {
case LOGIN: {
console.log("ACTION AT MODULES");
console.log(action);
localStorage.setItem("isLoggedIn", JSON.stringify(action.payload.isLoggedIn))
localStorage.setItem("user", JSON.stringify(action.payload.user))
return {
...state,
isLoggedIn: action.payload.isLoggedIn,
user: action.payload.user
};
}
case LOGOUT: {
localStorage.clear()
return {
...state,
isLoggedIn: false,
user: null
};
}
default:
return state;
}
};
- login, logout action이 선언되었으며 login이 될 때, localStorage에 user 정보와 login되었는지를 판단하는 boolean을 저장한다.
- logout 액션 호출 시 저장된 localStorage를 삭제하고 login 상태를 false로 변경한다.
같은 modules 폴더 아래에 index.js 파일을 생성하고 아래와 같이 작성한다.
import { combineReducers } from 'redux';
import login from './login';
const rootReducer = combineReducers({
login,
});
export default rootReducer;
이제 login state 사용할 준비가 되었다.
3. Components 생성
src 폴더 아래에 components 파일을 생성하고 Home.js, Login.js 파일을 생성한다. 그리고 빈 파일인 상태로 놔두자.
src/App.js 파일을 먼저 수정하자
import './App.css';
import rootReducer from "./modules";
import {createStore} from "redux";
import { Provider } from 'react-redux';
import {composeWithDevTools} from "redux-devtools-extension";
import {HashRouter as Router,Route, Routes} from "react-router-dom";
import Login from "./components/Login"
import Home from "./components/Home"
const store = createStore(rootReducer,composeWithDevTools()); // 스토어를 만듭니다.
function App() {
return (
<Provider store={store}>
<Router>
<Routes>
<Route path="/login" element={<Login/>}>
</Route>
<Route path="/" element={<Home/>}>
</Route>
</Routes>
</Router>
</Provider>
);
}
export default App;
- createStore을 실행하여 state저장소가 되는 store를 선언하고, App() rendering할 때 Provider로 감싸준다.
- Route 경로가 설정되는데, login이 안되어있다면 login 화면으로, login이 되어있다면 home 화면으로 이동하도록 할 예정이다.
이제 먼저 Home 화면을 작성한다. src/components/Home.js에 아래와 같이 작성하자
import React from "react";
import { Navigate } from "react-router-dom";
import Styled from "styled-components";
import {shallowEqual, useDispatch, useSelector} from "react-redux";
import {actionLogout} from "../modules/login";
export default function Home() {
const { state } = useSelector(state => ({
state: state.login,
}),
shallowEqual
);
const dispatch = useDispatch();
if (!state.isLoggedIn) {;
return <Navigate to="/login" />;
}
const { avatar_url, name, public_repos, followers, following } = state.user
const handleLogout = () => {
dispatch(actionLogout());
}
return (
<Wrapper>
<div className="container">
<button onClick={()=> handleLogout()}>Logout</button>
<div>
<div className="content">
<img src={avatar_url} alt="Avatar"/>
<span>{name}</span>
<span>{public_repos} Repos</span>
<span>{followers} Followers</span>
<span>{following} Following</span>
</div>
</div>
</div>
</Wrapper>
);
}
const Wrapper = Styled.section`
.container{
display: flex;
flex-direction: column;
height: 100vh;
font-family: Arial;
button{
all: unset;
width: 100px;
height: 35px;
margin: 10px 10px 0 0;
align-self: flex-end;
background-color: #0041C2;
color: #fff;
text-align: center;
border-radius: 3px;
border: 1px solid #0041C2;
&:hover{
background-color: #fff;
color: #0041C2;
}
}
>div{
height: 100%;
width: 100%;
display: flex;
font-size: 18px;
justify-content: center;
align-items: center;
.content{
display: flex;
flex-direction: column;
padding: 20px 100px;
box-shadow: 0 1px 4px 0 rgba(0, 0, 0, 0.2);
width: auto;
img{
height: 150px;
width: 150px;
border-radius: 50%;
}
>span:nth-child(2){
margin-top: 20px;
font-weight: bold;
}
>span:not(:nth-child(2)){
margin-top: 8px;
font-size: 14px;
}
}
}
}
`;
- component 구성을 위한 styled를 제외하면 간단한 내용인데, login이 되어있지 않다면, login 화면으로 이동하고, 로그인 되어있다면 user의 정보를 활용하여 화면을 나타낸다.
이제 마지막으로 가장 중요한 src/components/Login.js Component를 작성하자.
import React, { useState, useEffect } from "react";
import { Navigate } from "react-router-dom";
import Styled from "styled-components";
import GithubIcon from "mdi-react/GithubIcon";
import { useSelector, useDispatch, shallowEqual } from 'react-redux';
import { actionLogin } from "../modules/login";
export default function Login() {
const { state, client_id, redirect_uri } = useSelector(state => ({
state: state.login,
client_id : state.login.client_id,
redirect_uri: state.login.redirect_uri
}), shallowEqual);
const dispatch = useDispatch();
const [data, setData] = useState({ errorMessage: "", isLoading: false });
console.log("IT IS LOGIN PAGE STATE")
console.log(state)
useEffect(() => {
// After requesting Github access, Github redirects back to your app with a code parameter
const url = window.location.href;
const hasCode = url.includes("?code=");
if(state.isLoggedIn){
return;
}
// If Github API returns the code parameter
if (hasCode) {
const newUrl = url.split("?code=");
window.history.pushState({}, null, newUrl[0]);
setData({ ...data, isLoading: true });
const requestData = {
code: newUrl[1]
};
console.log(requestData);
const proxy_url = state.proxy_url;
// Use code parameter and other parameters to make POST request to proxy_server
fetch(proxy_url + 'authenticate', {
method: "POST",
body: JSON.stringify(requestData)
})
.then(response => response.json())
.then(data => {
console.log("STATE IN useEffect IS BELOW")
console.log(state)
dispatch(actionLogin({ user: data, isLoggedIn: true }));
})
.catch(error => {
setData({
isLoading: false,
errorMessage: "Sorry! Login failed"
});
});
}
}, [state, data, dispatch]);
if (state.isLoggedIn) {
console.log("VERIFY LOGGED IN ! !")
return <Navigate to="/" />;
}
return (
<Wrapper>
<section className="container">
<div>
<h1>Welcome</h1>
<span>Super amazing app</span>
<span>{data.errorMessage}</span>
<div className="login-container">
{data.isLoading ? (
<div className="loader-container">
<div className="loader"></div>
</div>
) : (
<>
{
// Link to request GitHub access
}
<a
className="login-link"
href={`https://github.com/login/oauth/authorize?scope=user&client_id=${client_id}&redirect_uri=${redirect_uri}`}
onClick={() => {
setData({ ...data, errorMessage: "" });
}}
>
<GithubIcon />
<span>Login with GitHub</span>
</a>
</>
)}
</div>
</div>
</section>
</Wrapper>
);
}
const Wrapper = Styled.section`
.container {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
font-family: Arial;
> div:nth-child(1) {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
box-shadow: 0 1px 4px 0 rgba(0, 0, 0, 0.2);
transition: 0.3s;
width: 25%;
height: 45%;
> h1 {
font-size: 2rem;
margin-bottom: 20px;
}
> span:nth-child(2) {
font-size: 1.1rem;
color: #808080;
margin-bottom: 70px;
}
> span:nth-child(3) {
margin: 10px 0 20px;
color: red;
}
.login-container {
background-color: #000;
width: 70%;
border-radius: 3px;
color: #fff;
display: flex;
align-items: center;
justify-content: center;
> .login-link {
text-decoration: none;
color: #fff;
text-transform: uppercase;
cursor: default;
display: flex;
align-items: center;
height: 40px;
> span:nth-child(2) {
margin-left: 5px;
}
}
.loader-container {
display: flex;
justify-content: center;
align-items: center;
height: 40px;
}
.loader {
border: 4px solid #f3f3f3;
border-top: 4px solid #3498db;
border-radius: 50%;
width: 12px;
height: 12px;
animation: spin 2s linear infinite;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
}
}
}
`;
login을 위한 간단한 화면 구성이다.
- 먼저 LOGIN WITH GITHUB이라는 button이 있는데, 이 버튼을 클릭하면 제일 처음 git OAuth 페이지에서 app을 만들고 할당받은 clientID와 redirect uri(생략가능)을 이용하여 code 정보를 받아온다.
- code정보는 git OAuth 앱으로부터 token정보를 받아오는데 활용되며 받아온 code와 OAuth 페이지에서 확인 가능한 secret key를 이용하면 token을 받아올 수 있다.
- 위 과정은 현재 url에 code가 포함되어있는지 확인하는 useEffect 함수에서 진행되며 이미 눈치챈 분들이 있겠지만, token을 받아오기위한 github api 주소를 사용하지 않고 state.proxy_url로 요청함을 알 수 있다.
- proxy_url을 사용하는 이유는 CORS 이슈 때문인데, 이러한 문제를 해결하기 위해 별도의 간단한 proxy server를 만들어야 한다.
express Proxy Server 생성
project의 root dir에 (src 폴더 아님) server 폴더를 만들고 config.js파일을 하나 만든다음 아래와 같이 필요정보를 입력한다.
module.exports = {
client_id: 'YOUR_CLIENT_ID',
redirect_uri: 'http://localhost:3000/',
client_secret: 'YOUR_SECRET',
};
그 다음 같은 경로에 index.js 파일을 생성한다.
const express = require("express");
const bodyParser = require("body-parser");
const FormData = require("form-data");
const axios = require("axios");
const { client_id, redirect_uri, client_secret } = require("./config");
const app = express();
app.use(bodyParser.json());
app.use(bodyParser.json({ type: "text/*" }));
app.use(bodyParser.urlencoded({ extended: false }));
// Enabled Access-Control-Allow-Origin", "*" in the header so as to by-pass the CORS error.
app.use((req, res, next) => {
res.header("Access-Control-Allow-Origin", "*");
next();
});
app.post("/authenticate", (req, res) => {
const { code } = req.body;
const data = new FormData();
data.append("client_id", client_id);
data.append("client_secret", client_secret);
data.append("code", code.split('#/')[0]);
data.append("redirect_uri", redirect_uri);
// Request to exchange code for an access token
axios(`https://github.com/login/oauth/access_token?client_id=${client_id}&client_secret=${client_secret}&code=${code}`, {
method: "POST",
body: data,
})
.then((response) => {
console.log(response.data);
// response.data.text();
return response.data;
})
.then( async (paramsString) => {
let params = new URLSearchParams(paramsString);
const access_token = params.get("access_token");
console.log(access_token)
// Request to return data of a user that has been authenticated
var res = await axios(`https://api.github.com/user`, {
headers: {
Authorization: `bearer ${access_token}`,
},
});
return res.data;
})
// .then((response) => response.json())
.then((response) => {
return res.status(200).json(response);
})
.catch((error) => {
console.log(error);
return res.status(400).json(error);
});
});
const PORT = process.env.SERVER_PORT || 5001;
app.listen(PORT, () => console.log(`Listening on ${PORT}`));
- proxy server 는 localhost의 5001 port를 Listen 하고있다.
- http://localhost:5001/authenticate 로 들어온 요청은 ₩https://github.com/login/oauth/access_token?client_id=${client_id}&client_secret=${client_secret}&code=${code}₩ 경로로 redirect된다.
위 요청이 정상적으로 처리되면 response를 통해 token값을 받아 올 수 있게된다.
최종 결과
전체 코드는 아래에서 참고하면 됩니다.
https://github.com/KoJJang/react_git_oatuh
'react' 카테고리의 다른 글
[react] git API를 이용해보기(1) (yarn, react-route, axios) (0) | 2022.03.21 |
---|