리액트-카카오 인증
http://localhost:3000/auth/kakao/callback?code=코드값
router활용
→ index.js
<Provider> {/* 리덕스 설정 - 단방향성 */}
<BrowserRouter>
<App />
</ BrowserRouter>
→ App.jsx
<Route path="/" element={<컴포넌트이름 props />} />
스프링-카카오인증
http://localhost:8000/auth/kakao/callback
MVC패턴
요청이 들어가는 곳과 응답이 나오는 곳이 분리되어 있다
DispatcherServlet
전달자역할, URL 요청에 따라 인터셉트
Controller
@Controller, @RestController → 매칭역할
Model
DataSet
View
Front-End
ModelAndView(스프링 레거시의 경우)에 담은 데이터를 View를 통해 반영시킴
사용자에게 입력받은 값 전달-ajax, fetch, axios, get, post-@RequestBody
렌더링 → return(태그) → View구성, 컴포넌트
데이터가 바뀐다 → state가 바뀜(useState) → 리렌더링 일어남
여러 테이블 join → 일부만 바뀌었을 때 비교 알고리즘 필요 → 그렇기에 비교에 유니크한 key값 필요
Flux 구조
단방향 데이터 흐름
Action - 변화 시그널, 요청
Dispatcher - 액션 정보 청취하고 Store에 전달, Hub의 역할
Store - Model, DataSet → 가장 상위인 index.js에 존재해야 함(이곳에 로그인 유지정보 가지고 있어야 한다)
index.js → App.jsx → 하위페이지로 전달되야 하는데 props로 전달하지 않고 Redux로 직접 전달 가능(단방향)
View - 응답 페이지, 새로운 요청이 생기면 다시 Action 시작
<카카오 로그인, 로그아웃 - index.html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script src="https://developers.kakao.com/sdk/js/kakao.min.js"></script>
<title>React App</title>
</head>
<body>
<div id="root"></div>
</body>
</html>
<카카오 로그인, 로그아웃 - App.jsx>
import { Route, Routes } from 'react-router-dom';
import './App.css';
import LoginPage from './components/auth/LoginPage';
import KakaoRedirectHandler from './components/kakao/KakaoRedirectHandler';
import Profile from './components/kakao/Profile';
import MemberPage from './components/page/MemberPage';
function App({imageUploader}) {
return (
<>
<Routes>
<Route path='/' exact={true} element={<LoginPage />} />
<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>
</>
);
}
export default App;
<카카오 로그인, 로그아웃 - KakaoRedirectHandler.jsx>
import axios from 'axios'
import React, { useEffect } from 'react'
import qs from 'qs'
import { useNavigate } from 'react-router-dom'
const KakaoRedirectHandler = () => {
// 카카오 객체를 global variable에 등록해주는 코드
// const {Kakao} = window
// location.href나 sendRedirect대신 사용함
const navigate = useNavigate()
// 카카오서버에서 돌려주는 URL 뒤 쿼리스트링 가져오기
// http://localhost:3000/auth/kakao/callback?code=코드값
// 자바스크립드에서 받기 - mdn searchParams
// 만약 서블릿에서 받으려면 - request.getParameter("code")
// searchParams - URL 내의 GET 디코딩 된 쿼리 매개변수에 접근할 수 있는 URLSearchParams 객체를 반환
let params = new URL(document.location).searchParams;
let code = params.get("code"); // 코드값
console.log(code)
const grant_type = "authorization_code"
const redirect_uri = "http://localhost:3000/auth/kakao/callback"
const getToken = async() => {
const payload = qs.stringify({
grant_type: grant_type,
client_id: process.env.REACT_APP_KAKAO_API_KEY,
redirect_uri: redirect_uri,
code: code
})
try {
const res = await axios.post(
"https://kauth.kakao.com/oauth/token",
payload
)
window.Kakao.init(process.env.REACT_APP_KAKAO_API_KEY)
console.log(res.data.access_token)
window.Kakao.Auth.setAccessToken(res.data.access_token);
navigate("/profile")
} catch (error) {
console.log(error)
}
}
useEffect (() => {
getToken()
})
return (
<div>
{/*
아무런 의미없는 화면 - 거쳐서 다른 화면으로 이동하니까
루트 컨텍스트 - 인증되면 /home으로 이동(Route에 설정됨)
*/}
{code}
</div>
)
}
export default KakaoRedirectHandler
<카카오 로그인, 로그아웃 - Profile.jsx>
import axios from 'axios'
import React, { useEffect, useState } from 'react'
import { useNavigate } from 'react-router-dom'
const Profile = () => {
console.log('Profile')
const navigate = useNavigate()
// 카카오에서 부여해준 아이디값
const [user_id, setUserId] = useState()
// 카카오에 등록된 사용자명
const [nickName, setNickName] = useState()
// 카카오에 등록된 프로필사진URL
const [profileImage, setProfileImage] = useState()
const getProfile = async () => {
try {
let data = await window.Kakao.API.request({
url: "/v2/user/me",
})
console.log(data.id)
console.log(data.properties.nickname)
console.log(data.properties.profile_image)
// 사용자 정보 변수에 저장 - 세션 지원안되기에 localStorage에 담기(로그아웃할떄 이 값 삭제해야함)
setUserId(data.id)
window.localStorage.setItem("userId", user_id)
setNickName(data.properties.nickname)
window.localStorage.setItem("nickname", nickName)
setProfileImage(data.properties.profile_image)
navigate("/home")
} catch (error) {
console.log(error)
}
}
useEffect(() => {
getProfile()
})
const kakaoLogout = async () => {
await axios({
method: "GET",
url: `https://kauth.kakao.com/oauth/logout?client_id=${process.env.REACT_APP_KAKAO_API_KEY}&logout_redirect_uri=http://localhost:3000`
}).then(res => { // 성공시 실행
console.log(res)
window.localStorage.removeItem("userId")
window.localStorage.removeItem("nickname")
navigate("/")
}).catch(error => { // 콜백에서 에러발생시 실행
console.log(error)
})
}
return (
<div>
<h3>{user_id}</h3>
<h3>{nickName}</h3>
<img src={profileImage} alt="프로필이미지"></img>
<br />
<button onClick={kakaoLogout}>카카오로그아웃</button>
</div>
)
}
export default Profile
<카카오 로그인, 로그아웃 - AuthController.java>
package com.example.demo.controller;
import javax.servlet.http.HttpSession;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.client.RestTemplate;
import com.example.demo.model.KakaoProfile;
import com.example.demo.model.OAuthToken;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;
@Controller
@RequestMapping("/auth/*")
public class AuthController {
Logger logger = LoggerFactory.getLogger(AuthController.class);
// 카카오 Redirect_url에 등록된 값으로 맞춤
@GetMapping("/kakao/callback")
public String kakaoCallback(HttpSession session, String code){
logger.info("kakaoCallback");
// POST방식으로 key=value 데이터를 요청 (카카오쪽으로)
// 이것을 위해 RestTemplate라이브러리가 있음
RestTemplate rt = new RestTemplate();
// HttpHeaders 객체 생성
HttpHeaders headers = new HttpHeaders();
headers.add("Content-type", "application/x-www-form-urlencoded;charset=utf-8");
MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
params.add("grant_type", "authorization_code");
params.add("client_id", "API키입력");
params.add("redirect_uri", "http://localhost:8000/auth/kakao/callback");
params.add("code", code);
// HttpHeader와 HttpBody를 하나의 오브젝트에 담기
HttpEntity<MultiValueMap<String, String>> tokenRequest = new HttpEntity<>(params, headers);
// Http요청하기 - Post방식으로 - 그리고 response 변수의 응답 받음
ResponseEntity<String> response = rt.exchange("https://kauth.kakao.com/oauth/token", HttpMethod.POST,
tokenRequest, String.class);
logger.info(code);
logger.info(response.getBody());
ObjectMapper objectMapper = new ObjectMapper();
OAuthToken oAuthToken = null;
try {
oAuthToken = objectMapper.readValue(response.getBody(), OAuthToken.class);
} catch (JsonMappingException jme) {
jme.printStackTrace();
} catch (JsonProcessingException je) {
je.printStackTrace();
}
logger.info(oAuthToken.toString());
logger.info("access_token ==> " + oAuthToken.getAccess_token());
// 사용자 정보 가져오기
RestTemplate rtUser = new RestTemplate();
// HttpHeaders 객체 생성
HttpHeaders headers2 = new HttpHeaders();
headers2.add("Authorization", "Bearer " + oAuthToken.getAccess_token());
headers2.add("Content-type", "application/x-www-form-urlencoded;charset=utf-8");
// HttpHeader와 HttpBody를 하나의 오브젝트에 담기
HttpEntity<MultiValueMap<String, String>> profileRequest = new HttpEntity<>(headers2);
// Http요청하기 - Post방식으로 - 그리고 response 변수의 응답 받음
ResponseEntity<String> response2 = rtUser.exchange("https://kapi.kakao.com/v2/user/me", HttpMethod.POST,
profileRequest, String.class);
System.out.println(response2.getBody());
ObjectMapper objectMapper2 = new ObjectMapper();
KakaoProfile kakaoProfile = null;
try {
kakaoProfile = objectMapper2.readValue(response2.getBody(), KakaoProfile.class);
} catch (JsonMappingException jme) {
jme.printStackTrace();
} catch (JsonProcessingException je) {
je.printStackTrace();
}
// User : username, password, email
logger.info("카카오 아이디(번호) : " + kakaoProfile.getId());
logger.info("카카오 이메일 : " + kakaoProfile.getKakao_account().getEmail());
logger.info("카카오 유저네임 : " + kakaoProfile.getProperties().nickname);
String nickname = kakaoProfile.getProperties().nickname;
session.setAttribute("nickname", nickname);
return "redirect:/";
}
}
<카카오 로그인, 로그아웃 - OAuthToken.java>
package com.example.demo.model;
import lombok.Data;
@Data
public class OAuthToken {
public String access_token;
public String token_type;
public String refresh_token;
public int expires_in;
public String scope;
public int refresh_token_expires_in;
public String id_token;
}
<카카오 로그인, 로그아웃 - KakaoProfile.java>
package com.example.demo.model;
import lombok.Data;
//https://www.jsonschema2pojo.org/에서 만들어줌
@Data
public class KakaoProfile {
public Long id;
public String connected_at;
public Properties properties;
public KakaoAccount kakao_account;
@Data
public class Properties {
public String nickname;
public String profile_image;
public String thumbnail_image;
}
@Data
public class KakaoAccount {
public Boolean profile_nickname_needs_agreement;
public Boolean profile_image_needs_agreement;
public Profile profile;
public Boolean has_email;
public Boolean email_needs_agreement;
public Boolean is_email_valid;
public Boolean is_email_verified;
public String email;
@Data
public class Profile {
public String nickname;
public String thumbnail_image_url;
public String profile_image_url;
public Boolean is_default_image;
}
}
}
Flux
Facebook에서 클라이언트-사이드 웹 어플리케이션을 만들기 위해 사용하는 어플리케이션 아키텍처
단방향 데이터 흐름을 활용해 뷰 컴포넌트를 구성하는 React를 보완하는 역할을 함
Dispatcher, Stores, Views의 핵심적인 세 가지 부분으로 구성됨
Dispatcher - 일꾼, Action을 Store에 전달
Store - 데이터를 가지고 있음
View
<Flux 예제 - app.js>
// 상태는 createStore() 안에 있다
const createStore = () => {
let state; // 상태를 담아두는 저장소
let handlers = [] // 함수를 담아두는 배열 선언
// 상태를 바꾸는 일을 send()에서 한다
const send = (action) => {
console.log('send 호출')
// 새로운 객체가 만들어진다 -> 깊은복사
// 아래 코드와 같은 원리
// Map m = new HashMap()
// m = new HashMap()
state = worker(state, action)
handlers.forEach(handler => handler())
}
const subscribe = ((handler) => {
handlers.push(handler)
})
const getState = () => {
return state;
}
// 함수 안에서 함수를 리턴하도록 처리해야 바깥쪽에서 해당 함수를 요청할 수 있다
return {
// state -> 이런식으로 직접적으로 상태값을 주지 않는다
// 밖에서 상태값을 변경해야하기에 send를 넘겨준다
send, // 함수(객체), 파라미터로 들어온 상태를 받아서 가공 후 새로운 객체로 내보냄
// 직접 상태값을 주지 못하지만 getState호출해 상태값 알 수 있게 함
getState, // 함수, 상태정보를 담은 state를 반환해줌
subscribe
}
}
// react-redux에서는 worker가 dispatcher가 됨
const worker = (state = {count: 0}, action) => { // state가 undefined되는 것 방지하기위해 객체 선언
// worker안에서는 무엇을 하는가?
// 상태를 바꾸면 createStore 안 state의 참조 무결성이 깨짐
// 리덕스에서는 상태를 바꾸는 함수는 반드시 새로운 상태를 반환해야한다
// 새로운 상태 -> 화면의 입력(Action)으로 상태의 객체를 줄테니 이 객체를 Deeo copy해서
// 기존의 참조링크를 끊어내라 - 그래야 side effect 방지 가능함
switch(action.type){
case 'increase':
return {...state, count: state.count + 1}
case 'decrease':
return {...state, count: state.count - 1}
default:
return {...state}
}
}
// 자바스크립트에서는 함수도 파라미터로 넘길 수 있다
const store = createStore(worker) // index.js에서 생성할 것임 - props대신 중앙에서 한번에 가져다 사용
store.subscribe(function () {
console.log(store.getState())
})
// action의 내용은 send에서 만듦
store.send({type: 'increase'}) // 시그널을 주기(어떤 타입을 바꿀건지) - action
store.send({type: 'increase'})
store.send({type: 'decrease'})
/*
send 호출
{ count: 1 }
send 호출
{ count: 2 }
send 호출
{ count: 1 }
*/
/*
JS에서 함수는 객체이다
소문자로 선언하면 함수이고
대문자로 선언하면 화면을 렌더링하는 컴포넌트이다
1. UI한테는 직접적인 상태를 주지 않는다
return에서는 상태값을 직접 넘겨주지 않는다
상태는 createStore함수에 있지만,
변경하거나 읽거나 하는 코드들은 UI의 Component들이다(컴포넌트가 데이터를 사용한다)
이 컴포넌트들은 createStore함수의 바깥쪽에 위치한다
-> 밖에서 createStore함수 안의 state를 변경하는 로직을 작성해야한다
-> worker를 createStore에 넘겨준다(createStore에서 변경할 값과 변경 내용을 알아야하기에)
-> 언제 무엇을 변경할 것인가 시그널은 store 밖에서 준다(ex. 사용자가 버튼을 누름)
문제제기
컴포넌트(HomePage.jsx, LoginPage.jsx 등)가 여러개 있는 상황에서 어떤 컴포넌트가
데이터가 변경되었는지 어떻게 알고서 getState함수를 호출할 것인가?
-> 구독발행 모델 사용 - Pub and Subscribe
일종의 패턴, 어떤 함수를 주고 데이터가 변경되면 그 함수를 호출해줌(이벤트처리)
*/
<카카오맵 - index.html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script src="https://developers.kakao.com/sdk/js/kakao.min.js"></script>
<script
type="text/javascript"
src="//dapi.kakao.com/v2/maps/sdk.js?appkey=%REACT_APP_KAKAO_JS_KEY%"
></script>
<title>React App</title>
</head>
<body>
<div id="root"></div>
</body>
</html>
<카카오맵 - 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";
const imageUploader = new ImageUploader();
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<>
<BrowserRouter>
<App imageUploader={imageUploader} />
</BrowserRouter>
</>
);
<카카오맵 - 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';
function App({imageUploader}) {
return (
<>
<Routes>
<Route path='/' exact={true} element={<LoginPage />} />
<Route path='/home' exact={true} element={<HomePage />} />
<Route path='/dept' exact={true} element={<DeptPage />} />
<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>
</>
);
}
export default App;
<카카오맵 - BlogHeader.jsx>
import React from 'react'
import { Container, Nav, Navbar } from 'react-bootstrap'
import { Link } from 'react-router-dom'
const BlogHeader = () => {
return (
<>
<Navbar bg="light" variant="light">
<Container>
<Link to="/" className='nav-link'>TerrGYM</Link>
<Nav className="me-auto">
<Link to="/home" className='nav-link'>Home</Link>
<Link to="/dept" className='nav-link'>부서관리</Link>
<Link to="/board" className='nav-link'>게시판</Link>
</Nav>
</Container>
</Navbar>
</>
)
}
export default BlogHeader
<카카오맵 - HomePage.jsx>
import React from 'react'
import BlogHeader from '../include/BlogHeader'
import KakaoMap from '../kakao/KakaoMap'
import {ContainerDiv, FormDiv, HeaderDiv} from '../styles/FormStyle'
const HomePage = () => {
return (
<>
<ContainerDiv>
<BlogHeader />
<HeaderDiv>
<h1 style={{marginLeft:"10px"}}>터짐블로그</h1>
</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
<카카오맵 - KakaoMap.jsx>
/* global kakao */
import React, { useEffect, useRef, useState } from 'react'
import { Button } from 'react-bootstrap';
import { BButton } from '../styles/FormStyle';
const KakaoMap = () => {
const kakaomap = useRef();
const [map, setMap] = useState()
const [positions, setPositions] = useState([
{
content: '<div>터짐블로그</div>',
latlng: new kakao.maps.LatLng(37.4984971, 127.032603)
}
])
useEffect(() => {
const container = document.getElementById("map")
const options = {
center: positions[0].latlng,
level: 4,
}
if(!map){
setMap(new kakao.maps.Map(container, options))
} else {
if(positions[1]) { // 자바스크립트에서는 0이 아닌 건 모두 true
map.setCenter(positions[1].latlng)
}
}
// 마커 표시하기
for(let i=0; i<positions.length; i++){
// 마커 생성하기
const marker = new kakao.maps.Marker({
map: map, // 마커를 표시할 지도
position: positions[i].latlng // 마커의 위치
})
// 마커에표시할 인포윈도우 생성하기
const infowindow = new kakao.maps.InfoWindow({
content: positions[i].content
});
// 마커에 이벤트를 등록하는 함수를 만들고 즉시 호출되도록 클로저 만듦
// 클로저를 추가하지 않으면 마커가 여러개 있을 때 마지막에만 이벤트가 적용됨
(function(marker, infowindow){
// 마커에 mouse over이벤트 등록하고 마우스 오버시 인포윈도우를 표시함
kakao.maps.event.addListener(marker, 'mouseover', function(){
infowindow.open(map, marker)
});
// 마커에 mouse out이벤트 등록하고 마우스 아웃시 인포윈도우를 닫음
kakao.maps.event.addListener(marker, 'mouseout', function(){
infowindow.close()
});
})(marker, infowindow)
} // end of for
}, [positions, map]);
return (
<>
<div style={{display:"flex", alignItems:"center", justifyContent:"space-around"}}>
<div id="map" ref={kakaomap} style={{width: "700px", height: "500px", marginBottom:"20px", border:"2px solid lightgray", borderRadius:"20px"}}></div>
</div>
<div style={{display:"flex", alignItems:"center", justifyContent:"space-around"}}>
<BButton type='button'>현재위치</BButton>
</div>
</>
)
}
export default KakaoMap
'국비학원 > 수업기록' 카테고리의 다른 글
국비 지원 개발자 과정_Day80 (0) | 2023.03.23 |
---|---|
국비 지원 개발자 과정_Day79 (0) | 2023.03.22 |
국비 지원 개발자 과정_Day77 (0) | 2023.03.20 |
국비 지원 개발자 과정_Day76 (0) | 2023.03.17 |
국비 지원 개발자 과정_Day75 (1) | 2023.03.16 |
댓글