기록
  • Day 5 - 로그인 기능 구현하기 2
    2024년 09월 10일 16시 24분 51초에 업로드 된 글입니다.
    작성자: 삶은고구마
    • 이메일, 비밀번호 인증 기능 사용해보기
    • 로그인/로그아웃 하기
    • 누이터에 소셜 로그인 추가하기
    • 네비게이션 추가 및 로그아웃 처리

     

    1.이메일, 비밀번호 인증 기능 사용해보기

    파이어베이스를 이용하여 회원가입 처리를 해보자.

    이메일 비밀번호 폼을 작성한 Auth.js를 복사하여 회원 가입 화면을 구현할 수 있다.

     

    1)onSubmit 함수에서 로그인과 회원가입 분기 시키기

    로그인,회원가입에 각각 onSubmit 함수를 1개씩 총 2개를 만들어야 한다고 생각하겠지만 

    상태를 잘 활용하면 2개를 만들 필요가 없다.

    Auth.js 파일을 수정해보자.

     

    1.newAccount를 useState 함수로 정의함

    const [newAccount,setNewAccount] = useState(true); 

    2.onSubmit에 분기 처리 상태가  newAccount면 새 계정을 추가, 아니면 로그인 처리

    if(newAccount) { 

    //create newAccount 

    } else{ 

    //log in 

    }

    3.value에 삼항연산자 처리

    <input type = "submit" value = {newAccount ? "Create Account" : "Log in"}/>

    -

    import { useState } from "react";
    const Auth = () =>{
        const [email,setEmail] = useState("");
        const [password,setPassword] = useState("");
        const [newAccount,setNewAccount] = useState(true);
        const onChange = (event)=>
        {
            // console.log(event.target.name);
            const{
                target : {name,value},
            } = event;
            if(name==="email"){
                setEmail(value);
            }
            else if(name==="password"){
                setPassword(value);
            }
        };
    
        const onSubmit = (event)=>
        {
            event.preventDefault();
            if(newAccount)
            {
                //create newAccount 
            }
            else{
                //log in
            }
        };
        return(
            <div>
                <form onSubmit={onSubmit}>
                    <input 
                        name="email" 
                        type = "email" 
                        placeholder="Email" 
                        required 
                        value={email} 
                        onChange={onChange}/>
                    <input 
                        name="password" 
                        type = "password" 
                        placeholder="Password" 
                        required
                        value={password}
                        onChange={onChange}/>
                    <input type = "submit" value = {newAccount ? "Create Account" : "Log in"}/>
                </form>
                <div>
                    <button>Continue with Google</button>
                    <button>Continue with Github</button>
                </div>
            </div>
        );
    };
    export default Auth;

     

     

    2)파이어베이스로 로그인과 회원가입 처리하기

    파이어베이스에서 제공하는 인증 기능 중 EmailAuthProvider 에 속하는

    1.CreateUserWithEmailAndPassword 함수 : 인자로 전달받은 이메일/비밀번호를 fb db에 저장

    2.signInWithEmailAndPassword 함수 : 인자로 전달받은 이메일/비밀번호를 fb db로 전달-확인하여 로그인

    두 개를 사용 할 것이다. 함수명이 꽤나 직관적이다.

    Auth.js 수정

    서버 수신 및 결과를 기다리기 위해 async와 await를 사용하였다.

    인증이 완료된 뒤에 누이터가 실행되어야 하기 때문이다.

    그리고 try-catch문은 예외처리를 위해 사용함. 

    //맨 윗줄에 import 추가
    import { authService } from "fbase";]
    .
    .
    (중략)
    const onSubmit = async (event)=>
        {
            event.preventDefault();
            try{
                let data;
                if(newAccount)
                    {
                        //create newAccount 
                        data = await authService.createUserWithEmailAndPassword(email,password);
                    }
                    else{
                        //log in
                        data = await authService.signInWithEmailAndPassword(email,password);
                    }
                    console.log(data);
                }
                catch(error)
                {
                    console.log(error);
                }
           
        };

     

    터미널 npm satrt로 실행후 결과 확인

    내 이메일, 비밀번호로 createAccount를 함.

    콘솔창을 보면 확인이 가능하다.

    만약 이 상태에서 아무것도 건드리지 않고 다시 create Account 버튼을 누를 경우 

    아래 빨간박스안의 로그가 뜨는데 , 이미 가입된 email이라고 알려주는 것이다. 즉, 중복까지 알려주는것임!

     

     

    그럼, 실제로 파이어베이스에 내가 등록한 회원 이메일이 db 저장되었는지 확인해보자.

    확인해보면 해당 이메일로 사용자가 회원가입 되어있다. :)

     

    아래 빨간박스안의 로그가 뜨는데 , 이미 가입된 email이라고 알려주는 것이다. 즉, 중복까지 알려주는것임!

     

    2)로그인을 지속시켜주는 setPersistence

    파이어베이스는 회원가입이 완료되면 로그인까지 처리해준다.

    로그인 상태 지속은 3가지로 나뉘어진다.

    1.local

    웹 브라우저를 종료해도 유지

    2.session

    웹 브라우저의 탭을 종료하면 로그아웃

    3.none

    새로고침하면 로그아웃! 말 그대로 사용자 정보를 기억하지 않는다.

    책에서는 local을 사용한다.

     

     

    3)사용자 정보가 저장되어 있는 곳은?

    local 옵션으로 저장한 사용자 로그인 정보는 브라우저 내의 indexedDB에 저장되어있다.

    따로 indexedDB관련 코드를 작성하지 않았지만 파이어베이스가 처리해줌.

     

     

    2.로그인/로그아웃 하기

    로그인 처리가 반영되었다면 App.js의 console.log(authService.currentUser); 로 출력되는 결과는 null이 아니여야 한다.

    하지만 현재 콘솔 창을 확인하면 null로 출력된다.

    파이어베이스에서 회원가입 및 로그인 처리를 마친 후에 누이터에 데이터를 보내주고

    그 데이터를 받아 누이터에서 이 후 화면을 그려줘야 하는데 지금 이 작업이 없기 때문에 

    로그인/회원가입이 되었어도 null이 출력되는 것이다.

    즉, 데이터 받기 전에 누이터에서 콘솔 로그로 출력해버려서 null값이 나오는 것임.

    리액트의 생명주기를 사용하면 해결이 가능하다.

     

    1)딜레이 눈으로 확인하기

    js 함수 중 setInterval 사용하기

    authService.currentUser 함수를 2초마다 함수를 실행시켜 로그값을 확인하는 것이다.

    App.js

    setInterval(()=>console.log(authService.currentUser),2000);

     

    실행결과:

    2초에 한 번 씩 currentUser 정보가 출력된다.

     

     

    2)useEffect 함수 사용하기

    userEffect 함수

    특정 시점에 실행되는 함수이다.

    내가 원하는 특정 시점은 파이어베이스 로그인 정보를 받게 되었을 때, 즉 파이어베이스가 초기화 되는 시점.

    그래야만 currentUser가 변경되는 시점을 확인하여 이 후 보여줄 화면을 렌더링 할 수 있다.

    파이어베이스의 authService에 포함된 함수 중 onAuthStateChanged를 사용하면 된다.

    말 그대로 인증상태가 변경되는 것을 감지하는 함수이다.

     App.js

    useEffect 함수의 두번째 인자로 []값을 사용한 이유?

    이렇게 해야 컴포넌트가 최초로 렌더링이 완료 되었을 때 1회만 동작.

    import { useEffect,useState } from "react";
    import AppRouter from "components/Router";
    import { authService } from "../fbase";
    
    function App() {
      const [isLoggedIn,setIsLoggedIn] = useState(authService.currentUser);
      useEffect(()=>{
        authService.onAuthStateChanged((user)=>console.log(user));
      },[]);
      //  setInterval(()=>console.log(authService.currentUser),2000);
      return (
        <>
        <AppRouter isLoggedIn={isLoggedIn}/>
        {/* jsx에 js 사용시 중괄호로 감싸기 */}
        <footer>&copy; {new Date().getFullYear()}Nwitter</footer>
        </>
      
      );
    }
    export default App;

     

     

    코드 작성을 하였다면 실행해서 결과를 확인해보자.

    firebaaseLocalStorage를 clear하자.

    그리고 새 이메일값으로 가입을 해보자.

    새 회원가입을 하면 console.log로 확인가능함.

    1.useEffect 함수에서 user를 체크 한 후

    2.setIsLoggedIn함수로 isLoggedIn를 변경해주고

    3.setInit 함수로 init 변경.

    4.user에 따라 라우터가 보여줄 화면을 다르게 렌더링한다.

     

    App.js

    authService.currentUser는 맨 처음 null 을 반환 하므로 isLoggedIn 역시 null을 반환할 가능성이 높다.

    불확실성을 제거하기 위해 isLoggedIn의 초깃값을 false로 지정하고 user에 값이 있을 때만

    isLoggedIn을 user로 설정하자. (값이 확실히 있을 때에만 사용하기 위해서 그런 것 같다)

    코드 수정 후 실행하면 initializing...  문구가 잠깐 떴다가 user가 존재한다면 즉, 정상적으로 로그인처리가 되었다면

    isLoggedIn에 user 값을 넘겨주고 home으로 이동한다.

    import { useEffect,useState } from "react";
    import AppRouter from "components/Router";
    import { authService } from "../fbase";
    
    function App() {
      const[init,setInit] = useState(false);
      const [isLoggedIn,setIsLoggedIn] = useState(false);
      useEffect(()=>{
        authService.onAuthStateChanged((user)=>{
          if(user) //여기서 로그인 상태 isLoggedIn 변경
          {
            setIsLoggedIn(user);
          }else
          {
            setIsLoggedIn(false);
          }
          setInit(true);//init상태 변경
        });
      },[]);
      //  setInterval(()=>console.log(authService.currentUser),2000);
      return (
        <>
        {init? <AppRouter isLoggedIn={isLoggedIn}/> : "initializing..."}
        {/* jsx에 js 사용시 중괄호로 감싸기 */}
        <footer>&copy; {new Date().getFullYear()}Nwitter</footer>
        </>
      
      );
    }
    export default App;

     

    3)로그아웃은?

    아직까진 firebaaseLocalStorage claer 로 데이터를 지워서 수동 로그아웃처리를 해야한다.

    그렇게 하면 Home화면에서 로그인화면으로 전환된다.

     

    4)에러와 에러 메시지를 파이어베이스로 처리하기

    로그인 할 수 없다거나, 비밀번호과 다르다거나, 이미 가입된 이메일이라거나 등등..

    사용자에게 알려줄 에러메시지를 화면단에 출력할 수 있다.

    Auth.js

    const Auth = () =>{
        const [email,setEmail] = useState("");
        const [password,setPassword] = useState("");
        const [newAccount,setNewAccount] = useState(true);
        const [error,setError] = useState("");

     

    에러가 발생할 지점에 setError 함수를 사용하면 된다.

     

    5)일부러 중복 회원가입을 하여 에러를 발생시켜본다.

     

    6)error.message 화면에 출력하기

    실제로 우리가 원하는 것 즉, 에러메시지는 error.message에 들어있다. 이걸 화면에 출력해야 한다!

    에러가 발생하면 setError 함수에 error.message를 전달하여 error 상태를 알려주자.

     

    Auth.js

    한칸 띄워 출력하기 위해 나는 <br>태그를 따로 사용했다

     <input type = "submit" value = {newAccount ? "Create Account" : "Log in"}/>
                    <br/>
                    {error}

     

    7)로그인, 회원가입 토글 버튼 적용하기

    토글 버튼 : 2가지 내용이 번갈아 바뀌는 버튼

    즉, 로그인 여부에 따라 버튼이 로그인/회원가입으로 전환되도록 만들 것이다!

     

    Auth.js

    토글 기능을 사용하기 위해 const toggleAccount = () => setNewAccount((prev)=>!prev); 를 추가했는데 

    요게 어떤거냐면..

    1. toggleAccount라는 함수를 선언한다. ()로 매개변수는 없음!

    2. setNewAccount((prev)=>!prev)는 함수 본문인데 이때, setNewAccount라는 함수가 호출된다.

    setNewAccount는 state를 업데이트하는 함수이며, useState를 통해 정의된 함수이다.

    3. ((prev)=>!prev)는 이전 상태 값을 받아서 그 값을 반전시킨다 값 앞에 !를 붙이면 반대값이 된다.

    prev는 이전 상태 값을 의미하고 !prev는 prev의 반대값이다 true->false /false->true가 됨.

     

    const toggleAccount = () => setNewAccount((prev)=>!prev);
        return(
            <div>
            ...
             </form>
                <div>
                    <span onClick={toggleAccount}>
                        {newAccount ? "Sign In" : "Create Account"}
                    </span>
                    <br/>
                    <button>Continue with Google</button>
                    <button>Continue with Github</button>
                </div>

     

    span태그 영역을 클릭하면 토글처럼 우측 버튼이 바뀜

     

     

    3.누이터에 소셜 로그인 추가하기

     

    파이어베이스의 signWithPopup함수를 사용한다.

     

    1)소셜 로그인 버튼에서 name 속성 사용

    구글, 깃 허브로 소셜 로그인을 하나의 함수로 처리한다.

    구글 / 깃허브 이 두개의 소셜 로그인을 구별하기 위해서 event.target.name 속성을 사용한다.

    이벤트에는 name 속성이 있으므로 이를 사용하여 소셜 로그인을 분기 처리한다.

     

    Auth.js

    기존에 만들어둔 구글,깃헙 로그인에 클릭 이벤트를 달아서 onSocialclick함수가 실행되도록 한다.

    그리고 name에 각각 google , github 값 속성 부여하여 event.target.name으로 출력을 확인한다.

    const toggleAccount = () => setNewAccount((prev)=>!prev);
    
        const onSocialClick = (event) =>{
            console.log(event.target.name);
        }
        ...
        <button onClick={onSocialClick} name="google">Continue with Google</button>
        <button onClick={onSocialClick} name="github">Continue with Github</button>

     

     

     

    2)소셜 로그인을 위한 firebaseInstance 추가

     

    현재 authService만 export 함

    소셜 로그인에 필요한 provider가 없다.

    해당 provider는 firebase에 들어있으므로 firebase 전체를 export한다.

     

    fbase.js 수정

    firebase.initializeApp(firebaseConfig);
    export const firebaseInstance = firebase;

     

     

    3)provider 적용 및 소셜 로그인 완성

    타겟 네임이 google이냐 github이냐에 따라 if~else if로 나누어서 각기 다른 소셜 로그인 서비스를 실행하도록 함.

    소셜 로그인 진행 함수인 signInWithPopup은 비동기 작업이라 async - await를 사용한다.

    *async - await 사용 이유

    코드 실행 순서를 명확히 하고 비동기 작업을 동기 작업처럼 처리

    가령 서버에서 데이터를 가져오거나, 파일 read write , db 접근 등..

    이런 작업들은 시간이 오래걸리기 때문에 결과가 나올 때까지 기다려야 하므로 사용한다(코드가 깔끔해짐)

    -async

    이 키워드를 붙인 함수는 자동으로 promise를 반환하는 비동기 함수가 된다.

    해당 함수가 종료되면 promise 반환

    -await

    비동기 작업이 완료될 때까지 기다리는 역할

    완료된 promise의 결과를 반환 하며 반드시 async 함수 안에서만 사용할 수 있음.

    이 키워드를 쓰면 마치 동기 코드 처럼 사용할 수있음.

    import { authService,firebaseInstance} from "fbase";
    import { useState } from "react";
    ..생략
    
        const onSocialClick = async (event) =>{
            //console.log(event.target.name);
            const{
                target : {name},
            }=event;
            let provider;
            if(name=="google")
            {
                provider = new firebaseInstance.auth.GoogleAuthProvider();
            }
            else if(name=="github")
            {
                provider = new firebaseInstance.auth.GithubAuthProvider();
            }
            const data = await authService.signInWithPopup(provider);
            console.log(data);
        };

     

     

    3.5)Error 발생

    소셜 로그인 팝업창이 뜨고 선택하면 해당 아이디로 로그인처리가 되는것 까지 확인했다.

    하지만 팝업창을 고의로 종료한다면 에러가 발생한다.

     

    Firebase: The popup has been closed by the user before finalizing the operation. (auth/popup-closed-by-user). FirebaseError: Firebase: The popup has been closed by the user before finalizing the operation. (auth/popup-closed-by-user). at createErrorInternal

     

    구글링해보니 인증 팝업을 사용자가 종료해버려서 발생하는 오류란다.

    -팝업창을 수동으로 닫았을 때

    -팝업 차단기에 의해 닫힌 경우

    -네트워크,버그 기타 문제로 팝업이 정상 작동하지 않음

     

    그래서 Auth.js 코드에 try-catch문을 사용해 이런 오류가 발생할 경우 에러페이지가 나타나지 않도록 처리했다.

    하단 catch부분에 실패했을 경우 처리를 함.

    errorcode는 아직 저 하나만 출력되어서 이 에러가 나올 경우 alert으로 팝업이 닫혔다 알려주고,

    나머지 에러는 로그로 출력되게 하였다.

    const onSocialClick = async (event) =>{
            //console.log(event.target.name);
            try{
            const{
                target : {name},
            } = event;
            let provider;
            if(name=="google")
            {
                provider = new firebaseInstance.auth.GoogleAuthProvider();
            }
            else if(name=="github")
            {
                provider = new firebaseInstance.auth.GithubAuthProvider();
            }
            const data = await authService.signInWithPopup(provider);
            console.log(data);
            }
            //팝업창을 강제로 닫거나 기타 등등 문제
            catch(error)
            {
                if (error.code === 'auth/popup-closed-by-user') {
                    // 사용자에게 팝업을 닫았음을 알리고 다시 시도하도록 유도
                    alert('팝업이 닫혔습니다. 다시 시도해주세요.');
                  } else {
                    // 다른 오류 처리
                    console.error(error);
                  }
            }
            };

     

     

     

     

    4.네비게이션 추가 및 로그아웃 처리

    1)네비게이션 컴포넌트 생성 후 라우터에 추가

    대부분의 페이지에서 보일 컴포넌트라 routes폴더에 만들지 않고 component에 생성.

    Navigation.js

    const Navigation = () =>{
        return <nav>This is Navigation!</nav>
    }
    
    export default Navigation;

     

     

    Router.js

    회원가입이나 로그인 페이지에서는 보이지 않고 로그인 후에 보이도록 하기 위해

    isLoggedIn이 true일때 네비게이션을 보여준다..

    import Navigation from "./Navigation";
    
    // 상위 컴포넌트에서 받은 프롭스를 구조분해 할당으로 사용한다. 
    const AppRouter = ({isLoggedIn}) =>
    {
        return(
            <Router>
              {isLoggedIn && <Navigation/>}
                    <Routes>

     

     

     

    2)네비게이션에 링크 추가

     

    Router.js

    import {Link} from "react-router-dom";
    
    const Navigation = () =>{
    
        return(
            <nav>
                <ul>
                    <li>
                        <Link to="/">Home</Link>
                    </li>
                    <li>
                        <Link to="/profile">My Profile</Link>
                    </li>
                </ul>
            </nav>
        ); 
    };
    
    export default Navigation;

     

     

    프로필을 누르면 링크는 변하지만 프로필 컴포넌를 렌더링 하지 않는다.

    다시 한 번 더 수정 하자.

    import { HashRouter as Router,Route,Routes } from "react-router-dom";
    import Auth from "routes/Auth";
    import Home from "routes/Home";
    import Profile from "routes/Profile";
    import Navigation from "./Navigation";
    
    // 상위 컴포넌트에서 받은 프롭스를 구조분해 할당으로 사용한다. 
    const AppRouter = ({isLoggedIn}) =>
    {
        return(
            <Router>
              {isLoggedIn && <Navigation/>}
                    <Routes>
            {isLoggedIn ? (
              <>
              <Route exact path="/" element={<Home />}></Route>
              <Route exact path="/profile" element={<Profile />}></Route>
              </>    
            ) : (
              <Route exact path="/" element={<Auth />}></Route>
            )}
          </Routes>
            </Router>    
        );
    };
    
    export default AppRouter;

     

    프로필 페이지가 렌더링됨

     

     

    3)로그아웃 추가

    profile컴포넌트에 로그아웃 버튼 추가

    signOut 함수가 indexedDB 정보를 비우고 로그아웃 처리까지 해준다.

    import { authService } from "fbase";
    const Profile = () => {
        const onLogOutClick = () => authService.signOut();
        return(
            <>
                <button onClick={onLogOutClick}>Log out</button>
            </>
        );
    };
    export default Profile;

     

     

     

    4)리다이렉트로 로그아웃 후 주소 이동

    로그아웃이 되긴 되었는데 뭔가 이상하다.

    나는 다시 로그인화면으로 되돌아가길 바랐는데 링크도 profile 그대로이다.

    이럴때 주소이동을 해야한다.

     

     

     

    여기서 오류가 발생한다.

    책에 기술된대로 import Redirect를 하려는데 아래와 같은 오류가 발생.

    export 'Redirect' (imported as 'Redirect') was not found in 'react-router-dom'

    구글링해보니 'react-router-dom' v6에서 Redirect가 사라지고 Navigate로 변경되었다고 한다;;

    네비게이트를 적용해서 코드를 수정하고 로그아웃 후 정상적으로 최상위루트로 렌더링되는걸 확인.

    import { HashRouter as Router,Navigate,Route,Routes } from "react-router-dom";
    import Auth from "routes/Auth";
    import Home from "routes/Home";
    import Profile from "routes/Profile";
    import Navigation from "./Navigation";
    
    // 상위 컴포넌트에서 받은 프롭스를 구조분해 할당으로 사용한다. 
    const AppRouter = ({isLoggedIn}) =>
    {
        return(
            <Router>
              {isLoggedIn && <Navigation/>}
                    <Routes>
            {isLoggedIn ? (
              <>
              <Route exact path="/" element={<Home />}></Route>
              <Route exact path="/profile" element={<Profile />}></Route>
              </>    
            ) : (
              <Route exact path="/" element={<Auth />}></Route>
            )}
            <Route path="*" element={<Navigate replace to="/" />} />    
          </Routes>
            </Router>    
        );
    };
    
    export default AppRouter;

     

     

    5)useHistory로 로그아웃 후 주소이동

    라우터가 아닌 useHistory를 사용해서 js로 리다이렉트 하는 방법도 있다.

    로그아웃을 처리하는 js코드 마지막에 처음화면으로 이동하라는 명령을 주는 것이다.

    useHistory에는 push라는 함수가 있는데 이 함수가 주소이동 역할을 한다.

    *브라우저에서는 useHistory를 사용해 사용자의 주소 이동 발자취를 기록한다.

    그럼 다시 Router.js에서 수정된 내용을 원복시키고 Profile.js에서 useHistory를 사용해보겠다.

     

    *오류발생

    export 'useHistory' (imported as 'useHistory') was not found in 'react-router-dom' 

    Redirect와 같은 사유로 사용할 수 없다. 

    다운그레이드를 하던가 Navigate를 사용해야 한다. 정상적으로 실행된다.

    import { authService } from "fbase";
    import {useNavigate } from "react-router-dom";
    
    const Profile = () => {
        const history = useNavigate ();
    
        const onLogOutClick = () => 
            {
                authService.signOut();
                history("/");
            };
        return(
            <>
                <button onClick={onLogOutClick}>Log out</button>
            </>
        );
    };
    export default Profile;

     

     

     

     

    github 커밋 완료

    댓글