4. Authentication
4-1. Using Firebase Auth
Router.js에 있던 useState를 App.js로 옮겨줌.(Router로서의 기능만 하도록 하기 위해서)
App.js에 useState를 import한 후, Router에 isLoggedIn을 prop으로 전달해줌.
App,js 안에 Router.js를 배치시켜주는 이유는?
다른 컴포넌트도 함께 사용 가능하기 때문.
페이지가 바뀌는 Router.js 영역과 계속하여 고정되어 있는 컴포넌트 영역을 같이 배치할 수 있음.
(ex. Header, Footer)
이런 식으로 사용 가능.
import { useState } from "react";
import "./App.css";
import Router from "./shared/Router";
import Header from "./components/Header"
import Footer from "./components/Footer"
function App() {
const [isLoggedIn, setIsLoggedIn] = useState(false);
return (
<Header />
<Router isLoggedIn={isLoggedIn} />
<Footer />
);
}
export default App;
❗ Firebase에서 Authentication에 관하여 찾기
Firebase의 규모가 매우 크기 때문에 스스로 찾아보는 방법을 익혀둬야 함.
- Firebase Docs 클릭
- Docs의 Reference 클릭
- Javascript - verson 9의 Overview firebase 클릭
- Packages 목록의 @firebase/auth 클릭
가장 먼저, auth를 사용하고 싶다면 firebase.js에 getAuth를 import 해줘야 함.
import 하지 않으면 Firebase에서 auth를 가져와 사용할 수 없음.
❗ Firebase에서 Authentication에 관하여 찾기2
auth를 어떻게 import하는지 등 세팅법을 알고 싶다면?
- Firebase Docs 클릭
- Docs의 Build 클릭
- Web의 Get Started 클릭
- Add and initialize the Authentication SDK
docs를 참고하여 firebase.js에 auth를 import해줌.
App.js에도 firebase.js를 import 해줌.
❗ react 절대경로 설정하기 - jsconfig.json
만약 나의 폴더 구조가 이렇다면?
import {...} from './components/폴더/폴더'
import {...} from '../../components/폴더/폴더'
import {...} from '../../../components/폴더/폴더'
코드양이 증가할 수록 import할 모듈도 많아지면서 점점 복잡해지고
새로운 파일에 import 내용을 복사 붙여넣기 한다면 다시 경로를 검토해야 함.
이때 절대경로로 할수있다면 깔끔하고 보기 좋을 거임.
- jsconfig.json 파일이 없는 경우 import 방법
import Button from '../../components/Button';
- jsconfig.json 파일이 있는 경우 import 방법
import Button from 'components/Button';
src 폴더와 동일한 위치에 jsconfig.json 파일을 새로 생성하여 코드를 입력.
{
"compilerOptions": {
"baseUrl": "src"
},
"include": [
"src"
]
}
이제, 유저를 가져와서 로그인 여부를 판단하도록 해야 함.
그러려면 authService.currentUser를 해주면 됨.
currentUser는 현재 로그인한 사용자를 알려줌. => User 또는 null을 반환
이걸 useState에 넣어줌
이제 useState는 User의 로그인 여부를 알 수 있게 되었음.
지금의 경우에는 로그인이 되어 있지 않아 Auth 화면이 뜸.
4-2. Login Form part One
현재 nwitter project의 Authentication에는 현재 아무 user도 없음.
login 방법도 설정되어 있지 않음.
Sign-in method를 클릭한 후 email / password를 선택.
google도 추가해줌.
SDK는 이미 설정되어 있으니 건드릴 필요가 없음.
github도 사용할 거임.
- github에 들어가서
- 메뉴에 있는 Setting를 클릭.
- Settings의 Developer settings를 클릭.
- OAuth Apps를 클릭 후, New OAuth App 생성
Application name은 Nwitter로 해주고,
Homepage URL은 Firebase에 있는 승인 콜백 URL에서 __/auth/handler을 뺀 것을,Authorization callback URL은 승인 콜백 URL을 그대로 가져다 쓰면 됨.
모든 작업이 끝나면 Register application을 클릭.
그렇게 해서 생성된 Client ID와 Client secrets key를 가져다 Firebase에 넣어주면 됨.
이렇게 email / password , Google, Github까지 세 개를 생성해줌.
렌더링 되어 화면에 보여질 코드들
- div 태그로 전체를 감싸주기.
- div 태그 내부에 form 태그 생성.
- form 태그 내부에 input 태그 생성. => type은 각각 text, password, submit
- div 태그 내부에 div 태그를 하나 더 생성.(form 태그와 같은 위치)
- 생성한 div 태그 내부에 button 태그를 두 개 생성. => 각각 Google과 Github로 로그인할 때 사용하려는 것들
렌더링되어 화면에 보여질 코드들을 다 작성하였다면,
Auth.js에 useState를 생성해줌.
하나는 email input에 value가 들어오도록 하기 위해서(email input에 글이 써지도록 하기 위해서),
하나는 password input에 value가 들어오도록 하기 위해서(password input에 글이 써지도록 하기 위해서)
그리고 각각의 input에 name과 value, onChange를 추가해줌.
=> <input name="email" type="text" value={email} onChange={onChange} />
=> <input name="password" type="password" value={password} onChange={onChange} />
form에 onSubmit를 추가해줌
=> <form onSubmit={onSubmit} />
Auth.js에 onChange 함수와 onSubmit 함수를 생성.
1. onChange 함수의 매개변수로 event를 넣어주고
내부에 const { target: { name, value } } = event; 를 적어줌. (event.target 내부에 name과 value가 있음)
구조분해시 이렇게 나타낼 수 있음.
=> const name = event.target.name;
=> const value = event.target.value;
그 후에 if문으로 name이 "email"일 때와 name이 "password"일 때로 나눠줌.
if ( name === "email" ) {
setEmail(value);
} else if {
setPassword(value);
}
onSubmit 함수 매개변수로 event를 넣어주고
내부에 event.preventDefault(); 를 적어줌. (submit이벤트 실행시 새로고침되는 것을 막아주기 위하여)
다 됐다면 email inpit type을 email로 바꿔주기.
4-3. Creating Account
- createUserWithEmailAndPassword
지정된 이메일 주소 및 비밀번호와 연결된 새 사용자 계정을 생성
사용자 계정이 성공적으로 생성되면 애플리케이션에 로그인됨.
계정이 이미 존재하거나 암호가 유효하지 않은 경우 사용자 계정 생성이 실패할 수 있음.
이메일 주소는 사용자의 고유 식별자 역할을 하며 이메일 기반 암호 재설정을 활성화.
이 기능은 새로운 사용자 계정을 생성하고 초기 사용자 암호를 설정.
오류 코드
1. auth/email-already-in-use
입력한 이메일 주소를 가진 계정이 이미 존재하는 경우 발생.
2. auth/invalid-email
이메일 주소가 유효하지 않은 경우 발생.
3. auth/operation-not-allowed
이메일/비밀번호 계정이 활성화되지 않은 경우 발생.
인증 탭 아래의 Firebase 콘솔에서 이메일/비밀번호 계정을 활성화.
4. auth/weak-password
약한 비밀번호암호가 충분히 강력하지 않은 경우 발생.
firebase.auth().createUserWithEmailAndPassword(email, password)
.catch(function(error) {
// Handle Errors here.
var errorCode = error.code;
var errorMessage = error.message;
if (errorCode == 'auth/weak-password') {
alert('The password is too weak.');
} else {
alert(errorMessage);
}
console.log(error);
});
- 계정이 있을 때와 계정이 없을 때를 분리시키기
가장 쉬운 방법은 state를 한 개 더 사용하는 것.
const [ newAccount, setNewAccount ] = useState(true);
newAccount가 true면 submit button을 "Create Account"로 바꾸고
newAccount가 false면 submit button을 "Log In"으로 바꿔줌.
- signInWithEmailAndPassword
이메일과 비밀번호를 사용하여 비동기식으로 로그인.
이메일 주소와 비밀번호가 일치하지 않으면 오류와 함께 실패.
사용자의 비밀번호는 사용자의 이메일 계정에 액세스하는데 사용되는 비밀번호가 아님.
이메일 주소는 사용자의 고유 식별자 역할을 하며 비밀번호는 Firebase 프로젝트에서 사용자 계정에 액세스하는데 사용됨.
오류 코드
1. auth/invalid-email
이메일 주소가 유효하지 않은 경우 발생.
2. auth/user-disabled
지정된 이메일에 해당하는 사용자가 비활성화된 경우 발생.
3. auth/user-not-found
주어진 이메일에 해당하는 사용자가 없는 경우 발생.
4. auth/wrong-password
주어진 이메일의 비밀번호가 유효하지 않거나 이메일에 해당하는 계정에 비밀번호가 설정되어 있지 않은 경우 발생.
example
firebase.auth().signInWithEmailAndPassword(email, password)
.catch(function(error) {
// Handle Errors here.
var errorCode = error.code;
var errorMessage = error.message;
if (errorCode === 'auth/wrong-password') {
alert('Wrong password.');
} else {
alert(errorMessage);
}
console.log(error);
});
코드 작성시 error 발생.
TypeError: fbase__WEBPACK_IMPORTED_MODULE_0__.authService.createUserWithEmailAndPassword is not a function
코드를 작성하면 함수가 아니라는 에러가 뜸.
구글링을 해본 결과,
Firebase가 버전 9로 업데이트 되면서 함수 사용방식이 달라진 거라고 함.
(Firebase 사용시 느끼는 고통...이랄까...)
해결 방법
import {
createUserWithEmailAndPassword,
signInWithEmailAndPassword,
} from "firebase/auth";
Auth.js에 위의 코드를 import해주고
data = await createUserWithEmailAndPassword(
authService,
email,
password
);
data = await signInWithEmailAndPassword(
authService,
email,
password
);
의 형태로 createUserWithEmailAndPassword 함수와 signInWithEmailAndPassword 함수를 사용.
이렇게 하면 Authentication에 12345@naver.com으로 사용자 정보가 들어와 있는 걸 볼 수 있음.(새로 만듦)
아직 화면에는 로그인된 게 보이지 않지만 로그인이 된 상태라고 보면 됨.
이제 해야 할 것은 setPersistence.
사용자들을 어떻게 기억할 것인지 선택할 수 있도록 해줌.
React Native 앱과 웹 브라우저에서의 초기값은 local.
persistence의 type
Enum | Value | Description |
firebase.auth.Auth.Persistence.LOCAL | 'local' | 'local'은 브라우저를 닫더라도 사용자 정보는 기억될 것이라는 의미. => 기본값. |
firebase.auth.Auth.Persistence.SESSION | 'session' | 'session'은 브라우저가 열려있는 동안에는 사용자 정보를 기억하는 것을 의미. |
firebase.auth.Auth.Persistence.NONE | 'none' | 유저를 기억하지 않는다는 것. => 로그인은 시켜주지만 새로고침을 하면 유저는 로그아웃 되어서 다시 로그인을 해야 함. |
4-4. Log In
어플리케이션이 로드될 때 Firebase는 사용자가 로그인이 되었는지 아닌지를 확인할 시간이 없음.
Firebase가 초기화되고 모든 걸 로드할 때까지 기다려줄 시간이 없기 때문.
그래서 어플리케이션이 시작되는 즉시 항상 로그아웃이 되어 있을 거임.
currentUser에는 아무것도 없을 거임.
- onAuthStateChanged
사용자의 로그인 상태의 변화를 관찰하는 관찰자를 추가.
4.0.0 이전에는 사용자가 로그인, 로그아웃할 때 또는 토큰 만료 또는 암호 변경과 같은 상황에서
사용자의 ID 토큰이 변경될 때 관찰자를 트리거했음.
4.0.0 이후에는 관찰자가 로그인 또는 로그아웃할 때만 트리거됨.
유저 상태에 변화가 있을 때 그 변화를 알아차림.
firebase.auth().onAuthStateChanged(function(user) {
if (user) {
// User is signed in.
}
});
App.js에 useState를 하나 더 추가해줌.
const [init, setInit] = useState(false);
현재 init의 값은 false로, 초기화가 이뤄지지 않았다는 뜻임.
Firebase가 프로그램을 초기화할 때까지 기다린 후 isLoggedIn이 바뀌도록 해야 함.
useEffect()를 사용하여 초기화하는 것을 기다릴 것임.
처음 시작할 때, 즉 컴포넌트가 mount 될 때 실행됨.
useEffect 안에 onAuthStateChanged를 넣어 사용자의 로그인 상태의 변화를 관찰할 거임.
새로고침하면 잠시 후 유저를 가지는 것을 볼 수 있음.
이렇게 실제로 로그인이 되었는지 안 되었는지를 알 수 있음.
authService.currentUser을 통해서는 로그인이 된 건지 로그아웃이 된 건지 잘 모르기 때문에 이게 필요함.
로그아웃 되었다고 알고 있는 것은 아마도 Firebase가 아직 시작되지도 않았기 때문임.
const [isLoggedIn, setIsLoggedIn] = useState(authService.currentUser); =>
const [isLoggedIn, setIsLoggedIn] = useState(false);
이렇게 초기값을 다시 false로 변경해준 후,
authService.onAuthStateChanged((user) => {
onAuthStateChanged 함수 안에 if문을 집어넣음.
만약 user가 들어왔으면 setIsLoggedIn(true);로 바뀌고
만약 user가 들어오지 않았으면 setIsLoggedIn(false);로 바뀜.
if문이 끝난 후에는 초기화가 끝났다는 뜻에서 setInit(true);로 바뀜.
});
이것을 통하여, 우리는 변화가 있는지를 보고 있는 거임.
누군가 Create Account button을 클릭하거나 Log In button을 눌렀거나 이미 로그인이 되어 있어서
Firebase는 스스로 초기화하는 것을 끝냄.
setIsLoggedIn(true);가 되면 Router.js의 로직에 따라 화면이 Home page로 바뀜.
그리고, init이 false;라면 Router를 숨길 거임.
init이 true;라면 Router를 보여주고, false라면 "Initializing..." 이라는 문구를 띄울 거임.
- 화면에 error 띄우기
Auth.js에 const [error, setError] = useState(""); 추가
그다음, onSubmit event를 실행했을 때 발생하는 error.message를 setError에 담아줌.
(이미 사용 중인 email이거나 비밀번호가 6자 미만이거나 하는 이유로)
그 다음, 렌더링 된 후 화면에서 보이도록 코드를 작성함.
그렇게 하면 이렇게 화면에서 오류 발생.
- Log In 기능 구현
이제 로그인 기능을 구현해보도록 할 거임.
newAccount 값을 바꿔줄 toggleAccount 함수를 선언해줌.
const toggleAccount = () => setNewAccount((prev) => !prev);
toggleAccount를 통하여 NewAccount의 이전 값을 반대로 바꿔줌.
span 태그를 하나 생성해 onClick event가 실행될 때 toggleAccount 함수가 함께 실행되도록 하고,
newAccount가 true면 "Sign In"을, false면 "Create Account"를 텍스트로 보이게끔 해줌.
그리고 submit button 역시
newAccount가 true면 "Create Account"를, false면 "Sign In"을 텍스트로 보이게끔 수정해줌.
이렇게 코드를 전부 작성해주면
submit button을 클릭시 onSubmit 함수가 실행되면서
newAccount가 true면 createUserWithEmailAndPassword()를,
newAccount가 false면 signInWithEmailAndPassword()를 실행시킴.
- 그렇다면 어떻게 작동을 하는 것인가?
span 태그인 Sign In을 클릭시 Create Account으로 바뀌고 newAccount의 값으로 false가 들어옴.
submit button의 value로는 Sign In이 들어옴.
email과 password를 넣어준 후 Sign In을 눌러주면 로그인이 됨.(조건 불만족시 error 발생)
반대로 span 태그인 Create Account를 클릭시 Sign In으로 바뀌고 newAccount의 값으로 true가 들어옴.
submit button의 value로는 Create Account가 들어옴.
email과 password를 넣어준 후 Create Account를 눌러주면 회원가입이 됨.(조건 불만족시 error 발생)
이렇게 회원가입과 로그인 기능을 끝마침.
4-5. Social Login
이번에는 google 또는 github로 로그인하기를 구현할 거임.
Firebase Authorization을 보면 SignInWithPopup 또는 signInWithRedirect 두 가지 옵션이 있음.
- SignInWithPopup
팝업 기반 OAuth 인증 흐름을 사용하여 Firevase 클라이언트를 인증.
성공하면 공급자의 자격 증명과 함께 로그인한 사용자를 반환.
로그인에 실패한 경우 오류에 대한 추가 정보가 포함된 오류 개체를 반환.
오류 코드
1. auth/account-exists-with-different-credential
자격 증명으로 확인된 이메일 주소가 있는 계정이 이미 있는 경우 발생.
error.email과 함께 firebase.auth.Auth.fetchSignInMethodsForEmail 을 호출 한 다음
반환된 공급자 중 하나를 사용하여 로그인하도록 사용자에게 요청하여 이 문제를 해결.
사용자가 로그인하면 error.credential에서 검색된 원래 자격 증명을 firebase.User.linkWithCredential을 사용하여
사용자에게 연결하여 사용자가 팝업 또는 리디렉션을 통해 원래 공급자에 다시 로그인하지 못하도록 할 수 있음.
로그인에 리디렉션을 사용하는 경우 세션 저장소에 자격 증명을 저장한 다음 리디렉션 시 검색하고
예를 들어 자격 증명을 다시 채움.
자격 증명 공급자 ID에 따라 firebase.auth.GoogleAuthProvider.credential 을 만들고 링크를 완료.
2. auth/auth-domain-config-required
firebase.initializeApp()을 호출할 때 authDomain 구성이 제공되지 않으면 발생.
해당 필드를 결정하고 전달하는 방법은 Firebase 콘솔을 확인할 것.
3. auth/cancelled-popup-request
연속 팝업 작업이 트리거되면 발생. 한 번에 하나의 팝업 요청만 허용.
마지막 팝업을 제외하고 모든 팝업이 이 오류와 함께 실패함.
4. auth/operation-not-allowed
자격 증명에 해당하는 계정 유형이 활성화되지 않은 경우 발생.
인증 탭 아래의 Firebase 콘솔에서 계정 유형을 활성화함.
5. auth/operation-not-supported-in-this-environment
애플리케이션이 실행 중인 환경에서 이 작업이 지원되지 않는 경우 발생.
"location.protocol"은 http 또는 https여야 함.
6. auth/popup-blocked
일반적으로 이 작업이 클릭 핸들러 외부에서 트리거될 때 팝업이 브라우저에 의해 차단된 경우 발생.
7. auth/popup-closed-by-user
공급자에 대한 로그인을 완료하지 않고 사용자가 팝업 창을 닫은 경우 발생.
8. auth/unauthorized-domain
앱 도메인이 Firebase 프로젝트의 OAuth 작업에 대해 승인되지 않은 경우 발생.
Firebase 콘솔에서 승인된 도메인 목록을 수정함.
example
// Creates the provider object.
var provider = new firebase.auth.FacebookAuthProvider();
// You can add additional scopes to the provider:
provider.addScope('email');
provider.addScope('user_friends');
// Sign in with popup:
auth.signInWithPopup(provider).then(function(result) {
// The firebase.User instance:
var user = result.user;
// The Facebook firebase.auth.AuthCredential containing the Facebook
// access token:
var credential = result.credential;
}, function(error) {
// The provider's account email, can be used in case of
// auth/account-exists-with-different-credential to fetch the providers
// linked to the email:
var email = error.email;
// The provider's credential:
var credential = error.credential;
// In case of auth/account-exists-with-different-credential error,
// you can fetch the providers using this:
if (error.code === 'auth/account-exists-with-different-credential') {
auth.fetchSignInMethodsForEmail(email).then(function(providers) {
// The returned 'providers' is a list of the available providers
// linked to the email address. Please refer to the guide for a more
// complete explanation on how to recover from this error.
});
}
});
본격적으로 social login 코드를 짜보도록 하겠음.
강의에서는 firebase.js에 export const firebaseInstance = firevaseConfig;를 해준 후
Auth.js에서 import { firebaseInstance } from "../firebase"를 해줘야 한다고 설명을 했는데
현재 Firebase verson을 기준으로는 이 두 개를 생략해도 별 다른 문제가 발생하지 않음.
그래서 나는 생략함.
Auth.js에 import { GithubAuthProvider, GoogleAuthProvider, signInWithPopup } from "firebase/auth"를 추가
그리고 onSocialClick 함수 내부에 코드를 작성해줌.
매개변수로 event를 주고
onSocialClick 함수 내부에서 const { target : { name } } = event; 를 선언.
구조 분해 할당된 것으로 풀어서 쓰면 const name= event.target.name; 과 같음.
최신 verson에 맞게 코드를 작성함.
if문을 내부에 집어넣고, name이 "google"일 때와 "github"일 때로 나눔.
name이 "google"일 때는 provider = new GoogleAuthProvider를,
name이 "google"일 때는 provider = new GithubAuthProvider를 작성해줌.
그리고 if문이 끝난 후 await signInWithPopup()authService, provider); 를 작성해줌.
난 이 코드가 어떻게 출력되는지 보고 싶었기 때문에 상수 data에 담아줌.
4-6. Log Out
- Navigation 페이지 만들기
components 폴더 내부에 Navigation.js를 생성.
react-router-dom의 Link를 이용하여 클릭시 원하는 page로 이동할 수 있게끔 함.
Router.js에도 Navigation component를 import하여
isLoggedIn이 true일 때 Navigation이 화면에 보이게끔 함.
Navigation.js에 넣은 Profile.js page를 인식할 수 있도록 isLoggedIn이 true일 때 Route로 Profile을 추가.
로그인이 되어 있지 않은 상태에서는 Auth page로 이동되며 Navigation이 보이지 않음.
이제 로그아웃 기능을 구현해보도록 하겠음.
signOut()는 현재 사용자를 로그아웃시켜줌.
Profile.js에 button을 하나 만들어 이름을 Log Out으로 설정.
그리고 button을 클릭시 onLogOutClick 함수가 실행되게끔 함.
onLogOutClick 함수 내부에는 authService.signOut(); 코드를 작성해줌.
하지만 로그아웃을 해도 계속 Profile page에 머물러 있음.
이러한 문제점 때문에 Redirect를 사용하여 랜더링되면 to의 지정된 경로로 이동하게끔 할 거임.
but, router verson이 6으로 업그레이드 되면서 호환성 문제로 Redirect의 기능이 사라지게 됨.
Redirect 대신 useNavigate hook이나 Navigation component를 사용하여 경로 이동을 시켜줄 수 있음.
- useNavigate :
useNavigate() 를 호출하면, 경로 이동 처리를 할 수 있는 "함수"를 반환.
이 함수를 이용해 뒤로 이동, 경로 이동 등을 처리하면 됨.
replace 옵션을 통해 history에 이력을 남길지 여부를 설정할 수 있음.
사용법 예시
import { useNavigate } from 'react-router-dom';
export default function Test() {
const navigate = useNavigate();
return (
<div>
<button onClick={()=>{navigate(-1)}}>history 뒤로 이동</button>
<button onClick={()=>{navigate("/admin")}}>절대 경로 이동</button>
<button onClick={()=>{navigate("../content")}}>상대 경로 이동</button>
<button onClick={()=>{navigate("/admin", { replace: true })}}>history 이력 안남김</button>
</div>
);
}
- Navigate :
렌더링될 때 현재 위치를 변경.
useNavigate 훅의 래퍼 컴포넌트.(인자도 동일)
useNavigate 를 사용할 수 없는 환경에서 경로 이동 처리에 사용
to, replace, state를 모두 사용할 수 있음.
<Route>의 path에 바로 지정할 수도 있음.
이를 이용해 404 에러 페이지 리다이렉션을 처리하기도 함.
function App() {
return (
<div className="App">
<Routes>
{/* 404 리다이렉트 처리 */}
<Route path="/*" element={<Navigate to="/"></Navigate>}></Route>
</Routes>
</div>
);
}
export default App;
사용법 예시 : <Navigate to="/이동시킬경로" />
import { Navigate } from 'react-router-dom';
export default function Redirect() {
return <Navigate to="/"></Navigate>;
}
이 코드는 렌더링되는 즉시 "/" 경로로 이동.
Router.js에서 isLoggedIn이 false일 때(로그인 되어 있지 않을 때)
Navigate component를 이용하여 렌더링시 Home page로 이동하게끔 코드를 작성.
path="*"는 모든 route가 다 렌더링시 "/"로 재이동하는 것.
Router.js에 Navigate를 추가해주는 방법 외에도
Profile.js에 useNavigate를 import해줘
onLogOutClick 함수 내부에서 작성된 authService.signOut(); 코드 뒤에 Navigate("/");를 넣어
로그아웃 후 Home Page로 이동하게끔 만들어도 됨.
'(심층)리액트' 카테고리의 다른 글
트위터 클론코딩(with firebase) - 하나의 이메일로 여러 계정 생성이 안되는 문제 해결 (0) | 2023.02.09 |
---|---|
트위터 클론코딩(with firebase) - 04 (0) | 2023.02.07 |
트위터 클론코딩(with firebase) - 02 (0) | 2023.02.01 |
트위터 클론코딩(with firebase) - 01 (6) | 2023.02.01 |
React Hooks - 03 / useState를 활용한 useTabs (0) | 2023.02.01 |
github : https://github.com/dnjfht
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!