일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
- userevent_tracker
- identifierForVender
- Redux
- web track
- Excel
- node
- Game js
- Flutter
- KakaoMap
- Three-fiber
- Image Resize typescript
- androidId
- Prism.js
- uint16array
- Raycasting
- REST API
- jszip
- webrtc
- typescript
- react
- Completer
- Three js
- code editor
- swagger-typescript-api
- RouteObserver
- three.js
- uint8array
- Babel standalone
- babel
- methodChannel
- Today
- Total
Never give up
React - Social login(Kakao, Naver , Facebook, Google) 본문
오늘은 전에 만들었던것들중 하나인 소셜 로그인 부분을 포스트 해보려고 합니다
로그인 별로 개발자 콘솔에서 설정까지 따로따로 포스트 하고 싶지만.. 귀찮아서1
다른 포스트도 밀린게 많아서 하나에 정리해봤습니다
추가로 설정부분은 다루지 않을예정이고, 페이지 디자인한 html과 css는 생략하겠습니다
(생략은 하되 github링크에 포함되어 있습니다)
service/social_login.js
import util from "util/util";
export default class SocialLogin {
// 카카오 로그인
kakaoLogin() {
const src = "https://developers.kakao.com/sdk/js/kakao.min.js"
let kakaoScript = util.checkScript('kakao', src, openPopup)
function openPopup() {
if (!Kakao.isInitialized()) {
Kakao.init(process.env.Kakao_Client_Id)
}
Kakao.Auth.login({
success: (_) => {
Kakao.API.request({
url: '/v2/user/me',
data: {
property_keys: ["kakao_account.email", "kakao_account.profile"]
},
success: (res) => {
// res.kakao_account.email
// res.kakao_account.profile.nickname
// res.kakao_account.profile.profile_image_url
console.log({ res })
util.removeScript(kakaoScript)
return res.kakao_account
},
fail: (err) => {
alert(`개인정보를 가져올 수 없습니다. ${JSON.stringify(err)}`)
}
})
},
fail: (err) => {
alert(`도메인을 확인해주세요. ${JSON.stringify(err)}`)
},
});
}
}
// 네이버 로그인
naverLogin(value = true) {
const src = "https://static.nid.naver.com/js/naveridlogin_js_sdk_2.0.2.js"
let naverScript = util.checkScript('naver', src, openPopup)
function openPopup() {
const naverLogin = new naver.LoginWithNaverId({
clientId: process.env.Naver_Client_Id,
callbackUrl: "http://127.0.0.1:3000/app/auth/naver",
isPopup: false,
loginButton: { color: "green", type: 3, height: 60 },
callbackHandle: true
});
naverLogin.init();
console.log({ naverLogin, naver })
if (value) {
const btn = document.getElementById('naverIdLogin').firstChild
btn.click()
return
}
naverLogin.getLoginStatus(function (status) {
if (status) {
const { email, name, profile_image } = naverLogin.user
console.log({ email, name, profile_image });
if (email === undefined || email === null || name === undefined || name === null) {
alert("이메일은 필수정보입니다. 정보제공을 동의해주세요.");
naverLogin.reprompt();
return;
}
util.removeScript(naverScript)
} else {
alert("callback 처리에 실패하였습니다.");
}
});
}
}
// 구글 로그인
setUpGoogleLogin() {
const src = "https://accounts.google.com/gsi/client"
let googleScript = util.checkScript('gsi/client', src, setUp, 'google')
function setUp() {
google.accounts.id.initialize({
client_id: process.env.Google_Client_Id,
callback: (res) => {
const payload = util.decodePaylod(res.credential)
console.log({ payload }, payload.name, payload.email, payload.picture)
util.removeScript(googleScript)
const headChildren = document.head.children
for (let i = 0; i < headChildren.length; i++) {
if (headChildren[i]?.id?.includes('googleidentityservice') ?? false) {
util.removeScript(headChildren[i])
}
}
}
});
google.accounts.id.renderButton(
document.getElementById('googleLogin'),
{ type: "icon", shape: 'circle' }
)
}
}
// 메타 로그인
async metaLogin() {
const src = "https://connect.facebook.net/en_US/sdk.js"
let metaScript = util.checkScript('facebook', src, openPopup, 'meta')
function openPopup() {
FB.init({
appId: process.env.Meta_Client_Id,
cookie: true,
xfbml: true,
version: 'v14.0'
});
FB.login((res) => {
if (res.status === 'connected') {
console.log({ res })
FB.api('/me', { fields: 'email,name,picture' }, (res2) => {
if (res2.err) {
alert(res2.err)
return
}
// res2.email
// res2.name
// res2.picture.data.url
console.log({ res2 })
util.removeScript(metaScript)
const headChildren = document.head.children
for (let i = 0; i < headChildren.length; i++) {
if ((headChildren[i]?.src?.includes('hash') ?? false)) {
util.removeScript(headChildren[i])
}
}
})
} else {
console.log({ res })
}
})
}
}
}
구글 로그인을 제외하고 전부 클릭 시점에 script 추가
그리고 onload 되는 시점에 로그인 기능을 구현해놨습니다
구글 로그인도 아래 메소드로 클릭하는 시점에 작동되도록 할수도 있는데
google.accounts.id.prompt
구글 로그인이 되어있는 상태가 아니면 작동이 안되는 이슈가 있어서
페이지가 로드되는 시점에 한번 로그인 버튼을 구현해놓는 로직을 만들어놨습니다
구글 로그인은 base64를 decoding 해주는 api를 제공해주지 않아서 따로 구현이 필요합니다
그리고 naver login을 보면 다른 소셜로그인과 달리 callbackurl에서 처리하는 부분이 있는데
로그인을 완료하고 나면, callback url로 이동하게 되고 query param에 값들이 입력되어
이 param들을 decoding해서 정보를 가져옵니다
- 추가 내용
작업하다보니 조금 더 좋은(?) 방식을 찾아서 변경해봤습니다
추가로 개판(?)으로 해놨던 폴더구조를 조금 개선해놨습니다
(아무리 간단한 예제를 만들더라도 신경을 조금 써야될거 같습니다..)
/components/common/social_btns.jsx
const SocialBtns = () => {
const login = new SocialLogin()
let isClicked = false
useEffect(() => {
login.setUpGoogleLogin()
}, [])
const socialLogin = [
{
src: KakaoLogo,
name: "카카오",
onClick: (e) => {
e.preventDefault()
clickBtn("kakao")
},
},
{
src: NaverLogo,
name: "네이버",
onClick: (e) => {
e.preventDefault()
clickBtn("naver")
},
},
{
src: MetaLogo,
name: "페이스북",
onClick: (e) => {
e.preventDefault()
clickBtn("meta")
},
},
{
src: GoogleLogo,
name: "구글",
onClick: (e) => {
e.preventDefault()
const div = document.querySelector(
"#googleLogin > div > div > div",
);
console.log({ div });
div.click();
},
},
]
const clickBtn = async (type) => {
if (isClicked) {
return
} else {
isClicked = true
util.delay(1000).then(() => {
isClicked = false
})
}
console.log('click')
switch (type) {
case 'kakao':
login.kakaoLogin()
break
case 'naver':
login.naverLogin()
break
case 'meta':
login.metaLogin()
break
}
}
return (
<>
<div className="div_social">
{socialLogin.map((e, i) => {
return (<img
key={`${e.name}`}
onClick={e.onClick}
src={e.src}
alt=""
/>)
})}
</div>
<div id="naverIdLogin" style={{ display: 'none' }} />
<div id="googleLogin" style={{ display: 'none' }} />
</>
)
}
export default SocialBtns
util에 script 추가, 제거, 중복체크등을 하는 친구들(?)이 있어서 한번 보도록 하겠습니다
util/util.js
import { Buffer } from "buffer"
const util = {
checkScript: (name, src, callback, type) => {
// name : script에 포함된 이름
// src : script내부에 사용되는 src
// callback : script 로드 완료 후 발생되는 콜백
// type : meta, google인 경우 추가적인 script 셋팅
const scriptList = document.getElementsByTagName('script')
let script
for (let i = 0; i < scriptList.length; i++) {
if (scriptList[i].src.includes(name)) {
script = scriptList[i]
}
}
if (script) {
callback()
} else {
script = document.createElement('script')
script.async = true
if (type === 'meta') {
script.defer = true
script.crossOrigin = "anonymous"
} else if (type === 'google') {
script.defer = true
}
script.src = src
document.head.appendChild(script)
script.onload = () => {
callback()
}
}
return script
},
decodePaylod: (token) => {
// base64 데이터 추출(google login)
const base64Payload = token.split('.')[1]
const payload = Buffer.from(base64Payload, 'base64')
return JSON.parse(payload.toString())
},
removeScript: (script) => {
// script 제거
document.head.removeChild(script)
},
delay: (ms) => {
// ms 만큼 딜레이
return new Promise((res) => {
const timer = setTimeout(() => {
res()
clearTimeout(timer)
}, ms);
})
},
}
export default util
먼저 checkScript는 여러가지 기능을 합쳐놨는데
1. head에 append되어있는 script를 확인하는 동작
2. script가 있으면 그 script사용, 없으면 추가
3. callback 실행
사실 분리를 해놓는게 유지보수 하기 좋을텐데 일단 안했습니다 귀찮아서2
decode payload는 위에 말씀드렸던 구글 로그인 base64값을 decode 해주는 코드입니다
그리고 필자가 구현한 화면은 다음과 같습니다
프로/아마추어 디자이너님들이 만들어놓으신 템플릿들 보고 대충 만들어봤습니다
다음은 로그인 버튼 눌렀을 때 출력값들입니다
(제 개인 계정들로 테스트해서 일단 값들은 가렸습니다..)
구글 로그인은 기본적으로 설정을 안해도 제공해주는 항목이 많아서
base64 decode하는 부분 그리고 script 추가하는 시점 부분 제외하고는 나름 간단했습니다
사실 만들면서 다른 로그인들은 뭔가 하자(?)가 하나씩 있었는데
카카오 로그인은 정말 깔끔하게 만들어 놓은거 같아서 구현하기 정말 편했던거 같습니다
다만 이메일 수집을 할 때, 필수로 하려면 심사 같은걸 받아야되는거 같았습니다
페북 로그인은 구현 자체는 간단했는데 스크린샷에서 res status에 connected가 아닌 unknown이 나오는 경우가 있습니다
예를들어 페북 로그인 안한 상태에서 버튼 누르고 로그인 하면 res2가 작동되지 않는 부분이 있어서
별도로 처리를 했는데, 해당 부분은 뭔가 완성된 코드라 하기가 애매해서 일단은 빼놨습니다
const fbLogin = () => FB.login((res) => {
if (res.status === 'connected') {
console.log({ res })
FB.api('/me', { fields: 'email,name,picture' }, (res2) => {
if (res2.err) {
messageStore.toast(res2.err)
return
}
// res2.email
// res2.name
// res2.picture.data.url
console.log({ res2 })
util.removeScript(metaScript)
const headChildren = document.head.children
for (let i = 0; i < headChildren.length; i++) {
if ((headChildren[i]?.src?.includes('hash') ?? false)) {
util.removeScript(headChildren[i])
}
}
})
} else if (res.status === 'unknown') {
fbLogin()
} else {
console.log({ res })
}
})
fbLogin()
해당 방식으로 하면 위 과정은 해결되는데
로그인 안하고 닫을때도 unknown이 뜨는거 같아서 잘 모르겠습니다
네이버로그인은 callback url에서 가져오는 부분이 조금 불편했고
버튼도 정해진 버튼을 사용해야되는데 다른 버튼들이랑 비슷하게 만들기 위해 조금 트릭을 썼습니다
1. display none을 이용해서 제공하는 버튼을 화면에서 없애줍니다
<div id="naverIdLogin" style={{ display: 'none' }} />
2. 해당 버튼을 click을 javascript로 구현합니다
const btn = document.getElementById('naverIdLogin').firstChild
btn.click()
3. 다른 버튼에 2번 코드를 넣어줍니다
해당 방식으로 간단하게 구현할 수 있었습니다
이번에 공유한 코드들은 svelte에서도 정상작동하고, 안해봤지만 vue에서도 정상작동해서
필요하시면 가져다 쓰셔도 됩니다
(전체 소스코드 : https://github.com/devmemory/react_social_login)
이번 포스트는 svelte로 먼저 구현을 하고 react로 옮겨서 포스트를 진행했는데
svelte로 개발하다가 react로 개발하니 귀찮고 불편했던것들이 많았던거 같습니다
물론 cra로 했으면 별도 셋팅으로 귀찮았던 부분을 해결할 수는 있었겠지만
기본적으로 제공해주는 부분들이 너무 차이가 나서 svelte를 계속 하고싶다는 생각이 들었습니다만..
일자리 문제로 react를 버릴수는 없을거 같다는 생각이 많이 들었습니다..
svelte가 vue시장 크기(?)정도로만 커지면 좋을거 같습니다
'WEB' 카테고리의 다른 글
React - html to doc (feat.quill2) (0) | 2022.11.15 |
---|---|
React - Drag & Drop file (0) | 2022.08.20 |
React - make html element to pdf(Feat. jspdf, html2canvas) (4) | 2022.07.02 |
React, Node - Graphql example(feat. apollo) (0) | 2022.05.24 |
React - Without CRA (0) | 2022.05.23 |