참고
- 댓글 기능 만들기 with react-comments-section
- 로그인한 사용자만 댓글 기능 사용하기
- GitHub RESTful API로 댓글 저장하기
- 리액트 쿠키로 GitHub OAuth 로그인 인증 관리하기
- Mui Pagination으로 댓글 페이지로 나누기
현재 로그인 인증은 로컬 스토리지를 이용하여 처리하였다.
react-cookie를 이용하여 로그인 인증 정보를 관리해보자.
로컬 스토리지와 쿠키의 차이는 다음과 같다.
로컬 스토리지 | 쿠키 | |
용량 | 최소 5MB 이상 | 최대 4KB |
전송 | 서버에 자동 전송 X (직접 구현) | HTTP 요청 헤더에 자동 포함 |
수명 | 만료 기간 미설정시 영구 유지 | 만료 날짜 또는 세션 동안 유지 |
접근성 | JavaScript로 읽고 쓰기 | JavaScript에서 읽을 수 없도록 설정 가능 |
보안 | 모든 js 코드에서 읽고 쓰기 때문에 민감한 정보는 저장 x |
HttpOnly로 읽을 수 없도록 설정 가능 Secure로 HTTPS 연결에서만 전송하도록 보호 |
쿠키가 세션 관리나 사용자 인증, 트래킹 등과 같은 작은 데이터를 저장할 때 조금 더 유용할 수 있다.
설치
npm install react-cookies --legacy-peer-deps
react-cookie 사용 예시
쿠키를 get/set/remove하는 예시는 다음과 같다.
import React from "react";
import { Cookies } from "react-cookie";
const cookies = new Cookies();
const ReactCookie = () => {
const setCookie = () => {
cookies.set("cookie_test", "testValue", {
path: "/",
maxAge: 3, // seconds
secure: true, // https 연결에만 전송
// httpOnly: true, // 클라이언트에서 read 불가
});
};
const getCookie = () => {
console.log(cookies.get("cookie_test"));
};
const removeCookie = () => {
cookies.remove("cookie_test");
};
return (
<div>
<button onClick={setCookie}>
쿠키 설정
</button>
<button onClick={getCookie}>
쿠키 확인
</button>
<button onClick={removeCookie}>
쿠키 제거
</button>
</div>
);
};
export default ReactCookie;
설정된 쿠키는 개발자 모드에서 Application 탭에서 확인 가능하다.
위의 코드에서 설정한대로 3초 동안 쿠키가 유지되는 것을 알 수 있다.
useCookies
react-cookie는 useCookies 훅을 제공한다. 예시는 다음과 같다.
import React, { useState, useEffect } from "react";
import { useCookies } from "react-cookie";
const ReactCookie = () => {
const [cookies, setCookie, removeCookie] = useCookies(["githubToken"]);
const [token, setToken] = useState(cookies.githubToken || null);
useEffect(() => {
const storedToken = cookies.githubToken;
if (storedToken) {
setToken(storedToken);
}
}, [cookies.githubToken]);
const handleLogin = () => {
// github oauth callback으로 token 획득
const receivedToken = "received_token_from_oauth";
setCookie("githubToken", receivedToken, {
maxAge: 3, // seconds
secure: true, // https 연결에만 전송
// httpOnly: true, // 클라이언트에서 read 불가
});
setToken(receivedToken);
};
const handleCheck = () => {
console.log(cookies);
};
const handleLogout = () => {
removeCookie("githubToken");
setToken(null);
};
return (
<div>
{token ? <p>login</p> : <p>logout</p>}
<button onClick={handleLogin}>로그인</button>
<button onClick={handleCheck}>쿠키 확인</button>
<button onClick={handleLogout}>로그아웃</button>
</div>
);
};
export default ReactCookie;
마찬가지로 Application 탭에서 확인 가능하다.
useCookies도 정상적으로 쿠키를 설정하는지 확인해보자.
깃허브 OAuth 콜백 함수에 쿠키 적용하기
깃허브 코드를 아래와 같이 고치자.
여기서는 cookielibrary.js를 만들어서 get/set/remove를 할 수 있도록 하였다.
import { Cookies } from "react-cookie";
const cookies = new Cookies();
export const setCookies = (key, value, options) => {
return cookies.set(key, value, {...options})
}
export const getCookies = (key) => {
return cookies.get(key)
}
export const removeCookies = (key) => {
return cookies.remove(key);
}
githublibrary.js에서 로컬 스토리지 코드를 쿠키로 수정한다.
import * as ck from "./cookielibrary.js";
...
export const loginCheck = async (setLoginStatus) => {
let token = ck.getCookies("GITHUB_TOKEN");
...
};
export const getLoginStatus = async () => {
let token = ck.getCookies("GITHUB_TOKEN");
...
};
GitHubLoginCallback.js도 수정한다. 60초 동안 로그인이 유지되도록 옵션을 설정하였다.
...
import * as ck from "../cookielibrary.js";
...
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;
...
let options = {
path: "/",
maxAge: 60, // 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);
}
};
...
};
App.js에서 로그인 확인 코드를 쿠키로 변경한다.
...
import * as ck from "./cookielibrary.js";
const App = () => {
const [loginStatus, setLoginStatus] = useState(false);
const [currentUser, setCurrentUser] = useState(null);
useEffect(() => {
if(loginStatus === false) return;
let loginID = ck.getCookies("LOGIN_ID");
let url = ck.getCookies("AVATAR_URL");
let profile = `https://github.com/${loginID}`;
...
마지막으로 ReactComents.js의 getCurrentUser에도 쿠키를 처리한다.
참고로 쿠키가 없는 경우는 null이 아니라 undefined를 return 받는다.
const getCurrentUser = () => {
if (currentUser === null) {
let loginID = ck.getCookies("LOGIN_ID");
let url = ck.getCookies("AVATAR_URL");
let profile = `https://github.com/${loginID}`;
if (loginID === null || loginID === undefined) return null;
return {
currentUserId: loginID,
currentUserImg: url,
currentUserProfile: profile,
currentUserFullName: loginID,
};
}
return currentUser;
};
60초 동안 로그인이 유지되는지 직접 확인해보자.
전체 코드는 다음과 같다.
cookielibrary.js
import { Cookies } from "react-cookie";
const cookies = new Cookies();
export const setCookies = (key, value, options) => {
return cookies.set(key, value, {...options})
}
export const getCookies = (key) => {
return cookies.get(key)
}
export const removeCookies = (key) => {
return cookies.remove(key);
}
githublibrary.js
import axios from "axios";
import { Octokit } from "@octokit/rest";
import * as ck from "./cookielibrary.js";
const myKey = process.env.REACT_APP_MY_TOKEN;
const repo = `auto-test`;
export const fileRead = async (path) => {
try {
const octokit = new Octokit({
auth: myKey,
});
const result = await octokit.request(
`GET /repos/bloodstrawberry/${repo}/contents/${path}`,
{
owner: "bloodstrawberry",
repo: `${repo}`,
path: `${path}`,
encoding: "utf-8",
decoding: "utf-8",
}
);
return result;
} catch (e) {
console.log("error : ", e);
return undefined;
}
};
export const fileCreate = async (contents, path) => {
const octokit = new Octokit({
auth: myKey,
});
try {
const result = await octokit.request(
`PUT /repos/bloodstrawberry/${repo}/contents/${path}`,
{
owner: "bloodstrawberry",
repo: `${repo}`,
path: `${path}`,
message: "make comments",
committer: {
name: "bloodstrawberry",
email: "bloodstrawberry@github.com",
},
content: `${btoa(unescape(encodeURIComponent(`${contents}`)))}`,
headers: {
"X-GitHub-Api-Version": "2022-11-28",
},
}
);
return result.status;
}
catch (e) {
console.log("error : ", e);
return undefined;
}
};
export const loginCheck = async (setLoginStatus) => {
let token = ck.getCookies("GITHUB_TOKEN");
try {
const response = await axios.get("https://api.github.com/user", {
headers: {
Authorization: `Bearer ${token}`,
},
});
console.log(response);
setLoginStatus(true);
} catch (error) {
console.error("Error fetching user data:", error);
setLoginStatus(false);
}
};
export const getLoginStatus = async () => {
let token = ck.getCookies("GITHUB_TOKEN");
try {
const response = await axios.get("https://api.github.com/user", {
headers: {
Authorization: `Bearer ${token}`,
},
});
console.log(response);
return true;
} catch (error) {
console.error("Error fetching user data:", error);
return false;
}
};
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, // 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;
App.js
import React, { useEffect, useState } from "react";
import { Route, Link, Routes } from "react-router-dom";
import "./App.css";
// ...
import ReactComments from "./page/ReactComments";
import GitHubLoginCallBack from "./page/GitHubLoginCallback";
import * as gh from "./githublibrary.js";
import * as ck from "./cookielibrary.js";
const App = () => {
const [loginStatus, setLoginStatus] = useState(false);
const [currentUser, setCurrentUser] = useState(null);
useEffect(() => {
if(loginStatus === false) return;
let loginID = ck.getCookies("LOGIN_ID");
let url = ck.getCookies("AVATAR_URL");
let profile = `https://github.com/${loginID}`;
setCurrentUser({
currentUserId: loginID,
currentUserImg: url,
currentUserProfile: profile,
currentUserFullName: loginID,
});
}, [loginStatus]);
useEffect(() => {
gh.loginCheck(setLoginStatus);
}, []);
return (
<div className="App">
<div className="router">
<span>
<Link to="/comments">Comments</Link>
</span>
</div>
<div>
<Routes>
<Route
path="/comments"
element={<ReactComments currentUser={currentUser} />}
/>
<Route
path="/callback"
element={
<GitHubLoginCallBack
loginStatus={loginStatus}
setLoginStatus={setLoginStatus}
/>
}
/>
</Routes>
</div>
</div>
);
};
export default App;
ReactComments.js
import React, { useEffect, useState } from "react";
import { CommentSection } from "react-comments-section";
//import 'react-comments-section/dist/index.css';
import "../css/comment.css";
import * as gh from "../githublibrary.js";
import * as ck from "../cookielibrary.js";
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 getCurrentDateTime = () => {
const now = new Date();
const year = now.getFullYear();
const month = String(now.getMonth() + 1).padStart(2, "0");
const day = String(now.getDate()).padStart(2, "0");
const hours = String(now.getHours()).padStart(2, "0");
const minutes = String(now.getMinutes()).padStart(2, "0");
const seconds = String(now.getSeconds()).padStart(2, "0");
const formattedDateTime = `${year}_${month}_${day}_${hours}_${minutes}_${seconds}`;
return formattedDateTime;
};
const ReactComments = ({ currentUser }) => {
const [initComments, setInitComments] = useState([]);
const currentData = (data) => {
console.log("current data", data);
};
const getCurrentUser = () => {
if (currentUser === null) {
let loginID = ck.getCookies("LOGIN_ID");
let url = ck.getCookies("AVATAR_URL");
let profile = `https://github.com/${loginID}`;
if (loginID === null || loginID === undefined) return null;
return {
currentUserId: loginID,
currentUserImg: url,
currentUserProfile: profile,
currentUserFullName: loginID,
};
}
return currentUser;
};
const loadComments = async () => {
let commentDir = `comments`;
let fileList = await gh.fileRead(commentDir);
if (fileList === undefined) return; // 아무 댓글도 없는 경우
// original_comments.js 찾기
let originalComment = fileList.data.filter(
(file) => file.name === "original_comments.js"
);
let originalCommentData = [];
if (originalComment.length) {
let result = await gh.fileRead(originalComment[0].path);
if (result === undefined) return;
originalCommentData = JSON.parse(
decodeURIComponent(escape(window.atob(result.data.content)))
);
}
let commentsAction = fileList.data.filter(
(file) => file.name !== "original_comments.js"
);
for (let action of commentsAction) {
let result = await gh.fileRead(action.path);
if (result === undefined) {
console.error("error data");
continue;
}
//console.log(result.data.name);
let contents = JSON.parse(
decodeURIComponent(escape(window.atob(result.data.content)))
);
//console.log(contents);
if (contents.type === "submit") {
delete contents.type;
originalCommentData.push(contents);
} else if (contents.type === "delete") {
let comIdToDelete = contents.comIdToDelete;
let parentOfDeleteId = contents.parentOfDeleteId;
if (parentOfDeleteId === undefined) {
// 댓글 삭제
originalCommentData = originalCommentData.filter(
(data) => data.comId !== comIdToDelete
);
} else {
// reply 삭제
let parent = originalCommentData.filter(
(data) => data.comId === parentOfDeleteId
)[0];
parent.replies = parent.replies.filter(
(data) => data.comId !== comIdToDelete
);
}
} else if (contents.type === "reply") {
let repliedToCommentId =
contents.parentOfRepliedCommentId || contents.repliedToCommentId;
let parent = originalCommentData.filter(
(data) => data.comId === repliedToCommentId
)[0];
delete contents.type;
parent.replies.push(contents);
} else if (contents.type === "edit") {
let comId = contents.comId;
let parentOfEditedCommentId = contents.parentOfEditedCommentId;
if (parentOfEditedCommentId === undefined) {
// 댓글을 변경한 경우
let parentText = originalCommentData.filter(
(data) => data.comId === comId
)[0];
parentText.text = contents.text;
} else {
// reply를 변경한 경우
let parent = originalCommentData.filter(
(data) => data.comId === parentOfEditedCommentId
)[0];
let child = parent.replies.filter((data) => data.comId === comId)[0];
child.text = contents.text;
}
}
}
setInitComments(originalCommentData);
if (commentsAction.length === 0) return;
// 기존 data는 삭제
let originalPath = `comments/original_comments.js`;
let result = await gh.fileDelete(originalPath);
console.log(`${originalPath} delete : ${result}`);
// 새로 생성
result = await gh.fileCreate(
JSON.stringify(originalCommentData, null, 2),
originalPath
);
console.log(result);
// 불필요한 action 삭제
for (let action of commentsAction) {
result = await gh.fileDelete(action.path);
console.log(`${action.path} delete : ${result}`);
}
};
useEffect(() => {
loadComments();
}, []);
const saveCommentAction = async (data) => {
console.log("save Comments Action");
let path = `comments/${getCurrentDateTime()}_data.json`;
let result = await gh.fileCreate(JSON.stringify(data, null, 2), path);
console.log(result);
};
const onSubmitAction = (data) => {
console.log("check submit, ", data);
data.type = "submit";
saveCommentAction(data);
};
const onDeleteAction = (data) => {
console.log("check Delete, ", data);
data.type = "delete";
saveCommentAction(data);
};
const onReplyAction = (data) => {
console.log("check Reply, ", data);
data.type = "reply";
saveCommentAction(data);
};
const onEditAction = (data) => {
console.log("check Edit, ", data);
data.type = "edit";
saveCommentAction(data);
};
return (
<div>
<CommentSection
currentUser={getCurrentUser}
logIn={{
loginLink: loginURL,
signupLink: "https://github.com/",
}}
commentData={initComments}
currentData={currentData}
onSubmitAction={onSubmitAction}
onDeleteAction={onDeleteAction}
onReplyAction={onReplyAction}
onEditAction={onEditAction}
hrStyle={{ border: "0.5px solid #ff0072" }}
inputStyle={{ border: "1px solid rgb(208 208 208)" }}
formStyle={{ backgroundColor: "white" }}
submitBtnStyle={{
border: "1px solid black",
backgroundColor: "black",
padding: "7px 15px",
}}
cancelBtnStyle={{
border: "1px solid gray",
backgroundColor: "gray",
color: "white",
padding: "7px 15px",
}}
replyInputStyle={{ borderBottom: "1px solid black", color: "black" }}
advancedInput={true}
removeEmoji={false}
/>
</div>
);
};
export default ReactComments;
'개발 > React' 카테고리의 다른 글
리액트 - IP 변환하기 (IP Converter) (0) | 2023.12.09 |
---|---|
React Material - Mui Pagination으로 댓글 페이지로 나누기 (0) | 2023.11.19 |
리액트 - GitHub RESTful API로 댓글 저장하기 with react-comments-section (0) | 2023.11.15 |
리액트 - 로그인한 사용자만 댓글 기능 사용하기 with react-comments-section (GitHub OAuth Login) (0) | 2023.11.15 |
리액트 - 댓글 기능 만들기 with react-comments-section (React Comments and Reply) (0) | 2023.11.15 |
댓글