본문 바로가기
IT/React

[React] 서버리스 메모앱-3

by 밤톨엽 2022. 4. 24.

해당 글은 [React] 서버리스 메모앱-2와 이어집니다.

이번 글에서는 AWS Cognito 로그인, 세션, 로그인&로그아웃에 대해서 작성을 하였습니다.


AWS Coginto로 로그인

이전에 설치해둔 AWS Amplify를 통하여 Cognito 설정에 로그인을 진행해보자.

src/containers/Login.tsx 파일에서 Auth 모듈을 추가해주자.

import { Auth } from "aws-amplify"; // 상단에 추가

다음은 handleSubmit() 함수를 아래처럼 변경해주자

async function handleSubmit(event: any) {
  event.preventDefault();

  try {
    await Auth.signIn(email, password);
    alert("로그인 성공");
  } catch (e: unknown) {
    alert((e as Error).message);
  }
}

변경한 src/containers/Login.tsx 전체 코드는 아래와 같다.

import React, { useState } from "react";
import Form from "react-bootstrap/Form";
import Button from "react-bootstrap/Button";
import { Auth } from "aws-amplify"; // 상단에 추가
import "./Login.css";

export default function Login() {
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");

  function validateForm() {
    return email.length > 0 && password.length > 0;
  }

  async function handleSubmit(event: any) {
    event.preventDefault();
  
    try {
      await Auth.signIn(email, password);
      alert("로그인 성공");
    } catch (e: unknown) {
      alert((e as Error).message);
    }
  }

  return (
    <div className="Login">
      <Form onSubmit={handleSubmit}>
        <Form.Group controlId="email">
          <Form.Label>이메일</Form.Label>
          <Form.Control
            autoFocus
            type="email"
            value={email}
            onChange={(e) => setEmail(e.target.value)}
          />
        </Form.Group>
        <Form.Group controlId="password">
          <Form.Label>비밀번호</Form.Label>
          <Form.Control
            type="password"
            value={password}
            onChange={(e) => setPassword(e.target.value)}
          />
        </Form.Group>
        <Button block size="lg" type="submit" disabled={!validateForm()}>
          로그인
        </Button>
      </Form>
    </div>
  );
}

마지막으로 이전에 가입해놓은 admin@example.com & Passw0rd! 정보로 로그인을 진행하자.

login success
login success

세션 추가

로그인은 성공했지만 로그인 상태를 유지하기 위해 React의 컴포넌트에 Props를 통해 넘겨주어도 괜찮지만 로그인 정보는 다른 화면에서도 많이 사용할 수 있는 정보이니 모든 컴포넌트에서 쉽게 사용할 수 있게 도와주는 Context API를 이용해보자

Props Render란?

 

Components and Props – React

A JavaScript library for building user interfaces

reactjs.org

src/App.tsx 파일에서 아래 부분들을 변경해주자.

import React, { useState } from "react"; // 내용변경

function App() {
  // 상태값 추가
  const [isAuthenticated, userHasAuthenticated] = useState<boolean>(false);

  return (
    	...
  )
}

Context에 세션 저장

이제 로그인 정보 저장을 위해 Context를 생성하자.

src/lib/contextLib.tsx 파일을 추가하고 아래처럼 코드를 구성해주자

import { useContext, createContext } from "react";

export type Authentication = {
  isAuthenticated: boolean
  userHasAuthenticated:(state: boolean) => void
}

// createContext API를 사용하여 새 컨텍스트를 만든다.
export const AppContext = createContext<Authentication | null>(null);

// AppContext 에 액세스하기 위해 React Hook을 사용한다.
export function useAppContext() {
  return useContext(AppContext);
}

src/App.tsx 파일에 Context를 사용할 수 있도록 아래처럼 구성해주자

import { AppContext } from "./lib/contextLib"; // 상단에 추가

// Context를 전달하기위해 Routes쪽에 감싸기
<AppContext.Provider value={{ isAuthenticated, userHasAuthenticated }}>
  <Routes />
</AppContext.Provider>

src/App.tsx 파일의 전체 코드는 아래와 같다.

import React, { useState } from "react";
import Navbar from "react-bootstrap/Navbar";
import "./App.css";
import Routes from "./Routes";
import { AppContext } from "./lib/contextLib";
import Nav from "react-bootstrap/Nav";
import { LinkContainer } from "react-router-bootstrap";

function App() {
  const [isAuthenticated, userHasAuthenticated] = useState<boolean>(false);

  return (
    <div className="App container py-3">
      <Navbar collapseOnSelect bg="light" expand="md" className="mb-3">
        <LinkContainer to="/">
          <Navbar.Brand className="font-weight-bold text-muted">
            Scratch
          </Navbar.Brand>
        </LinkContainer>
        <Navbar.Toggle />
        <Navbar.Collapse className="justify-content-end">
          <Nav activeKey={window.location.pathname}>
            <LinkContainer to="/signup">
              <Nav.Link>회원가입</Nav.Link>
            </LinkContainer>
            <LinkContainer to="/login">
              <Nav.Link>로그인</Nav.Link>
            </LinkContainer>
          </Nav>
        </Navbar.Collapse>
      </Navbar>
      <AppContext.Provider value={{ isAuthenticated, userHasAuthenticated }}>
        <Routes />
      </AppContext.Provider>
    </div>
  );
}

export default App;

Context로 상태 업데이트

Context.Provider에서 제공받은 상태 값을 사용하기 위해 src/containers/Login.tsx 파일을 아래처럼 구성해주자

import { Authentication, useAppContext } from "../lib/contextLib"; // 상단에 추가

const { userHasAuthenticated } = useAppContext() as Authentication; // function Login() 바로 아래 작성

// handleSubmit alert 변경
async function handleSubmit(event: any) {
    event.preventDefault();

    try {
      await Auth.signIn(email, password);
      userHasAuthenticated(true);
    } catch (e: unknown) {
      alert((e as Error).message);
    }
}

src/containers/Login.tsx 파일의 전체 코드는 아래와 같다.

import React, { useState } from "react";
import Form from "react-bootstrap/Form";
import Button from "react-bootstrap/Button";
import { Auth } from "aws-amplify";
import { Authentication, useAppContext } from "../lib/contextLib";
import "./Login.css";

export default function Login() {
  const { userHasAuthenticated } = useAppContext() as Authentication;
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");

  function validateForm() {
    return email.length > 0 && password.length > 0;
  }

  async function handleSubmit(event: any) {
    event.preventDefault();
  
    try {
      await Auth.signIn(email, password);
      userHasAuthenticated(true);
    } catch (e: unknown) {
      alert((e as Error).message);
    }
  }

  return (
    <div className="Login">
      <Form onSubmit={handleSubmit}>
        <Form.Group controlId="email">
          <Form.Label>이메일</Form.Label>
          <Form.Control
            autoFocus
            type="email"
            value={email}
            onChange={(e) => setEmail(e.target.value)}
          />
        </Form.Group>
        <Form.Group controlId="password">
          <Form.Label>비밀번호</Form.Label>
          <Form.Control
            type="password"
            value={password}
            onChange={(e) => setPassword(e.target.value)}
          />
        </Form.Group>
        <Button block size="lg" type="submit" disabled={!validateForm()}>
          로그인
        </Button>
      </Form>
    </div>
  );
}

로그아웃 버튼

로그인된 세션정보를 추가했으니 해당 정보로 로그아웃 버튼을 생성하자.

src/App.tsx 파일을 아래처럼 수정하자.

<LinkContainer to="/signup">
  <Nav.Link>Signup</Nav.Link>
</LinkContainer>
<LinkContainer to="/login">
  <Nav.Link>Login</Nav.Link>
</LinkContainer>

// 위에 부분을 아래처럼 변경
{isAuthenticated ? (
  <Nav.Link onClick={handleLogout}>Logout</Nav.Link>
) : (
  <>
    <LinkContainer to="/signup">
      <Nav.Link>회원가입</Nav.Link>
    </LinkContainer>
    <LinkContainer to="/login">
      <Nav.Link>로그인</Nav.Link>
    </LinkContainer>
  </>
)}

// return 문 위에 선언
function handleLogout() {
  userHasAuthenticated(false);
}

src/App.tsx 파일의 전체 코드는 아래와 같다.

import React, { useState } from "react";
import Navbar from "react-bootstrap/Navbar";
import "./App.css";
import Routes from "./Routes"; // 상단에 추가
import { AppContext } from "./lib/contextLib";
import Nav from "react-bootstrap/Nav"; // --> 상단에 추가
import { LinkContainer } from "react-router-bootstrap"; // --> 상단에 추가

function App() {
  const [isAuthenticated, userHasAuthenticated] = useState<boolean>(false);

  function handleLogout() {
    userHasAuthenticated(false);
  }

  return (
    <div className="App container py-3">
      <Navbar collapseOnSelect bg="light" expand="md" className="mb-3">
        <LinkContainer to="/">
          <Navbar.Brand className="font-weight-bold text-muted">
            Scratch
          </Navbar.Brand>
        </LinkContainer>
        <Navbar.Toggle />
        <Navbar.Collapse className="justify-content-end">
          <Nav activeKey={window.location.pathname}>
          {isAuthenticated ? (
            <Nav.Link onClick={handleLogout}>Logout</Nav.Link>
            ) : (
            <>
              <LinkContainer to="/signup">
                <Nav.Link>회원가입</Nav.Link>
              </LinkContainer>
              <LinkContainer to="/login">
                <Nav.Link>로그인</Nav.Link>
              </LinkContainer>
            </>
          )}
          </Nav>
        </Navbar.Collapse>
      </Navbar>
      <AppContext.Provider value={{ isAuthenticated, userHasAuthenticated }}>
        <Routes />
      </AppContext.Provider>
    </div>
  );
}

export default App;

logout
logout

로그인을 구성하였지만 문제는 새로고침 시에 유지되지 않는다. 해당 부분을 세션을 사용하여 로드하도록 구성해보겠다.

사용자 세션 로드

로그인 정보를 유지하려면 브라우저 세션에서 로그인 정보를 저장하고 로드해야 한다. 쿠키 또는 로컬 저장소를 사용하여 이를 수행할 수 있는 몇 가지 다른 방법이 있다.

AWS Amplify는 이 작업을 자동으로 수행해주며 로드만 해주면 된다.

메모 애플리케이션이 로드될 때 세션 정보를 로드하도록 설정하자. 이를 위해 useEffect 라는 또 다른 React Hook을 사용한다.

Auth.currentSession() 을 사용할 것이며 Promise 객체를 반환하기 때문에 앱의 나머지 부분이 로드된 후에만 사용할 준비가 되었는지 확인해야 한다.

우선 src/App.tsx 파일에서 로그인 상태를 다를 수 있는 상태 값을 구성하자.

import React, { useState, useEffect } from "react"; // UseEffect 추가
import { Auth } from "aws-amplify"; // 상단에 추가

const [isAuthenticating, setIsAuthenticating] = useState(true); // 기존 상태값 아래 추가

// 위에 상태값 바로 아래에 추가
useEffect(() => {
  onLoad();
}, []);

async function onLoad() {
  try {
    await Auth.currentSession();
    userHasAuthenticated(true);
  }
  catch(e) {
    if (e !== 'No current user') {
      alert(e);
    }
  }

  setIsAuthenticating(false);
}

src/App.tsx 파일의 전체 코드는 아래와 같다.

import React, { useState, useEffect } from "react";
import Navbar from "react-bootstrap/Navbar";
import "./App.css";
import Routes from "./Routes";
import { AppContext } from "./lib/contextLib";
import Nav from "react-bootstrap/Nav";
import { LinkContainer } from "react-router-bootstrap";
import { Auth } from "aws-amplify";

function App() {
  const [isAuthenticated, userHasAuthenticated] = useState<boolean>(false);
  const [isAuthenticating, setIsAuthenticating] = useState(true);

  useEffect(() => {
    onLoad();
  }, []);

  async function onLoad() {
    try {
      await Auth.currentSession();
      userHasAuthenticated(true);
    }
    catch(e) {
      if (e !== 'No current user') {
        alert(e);
      }
    }
  
    setIsAuthenticating(false);
  }

  function handleLogout() {
    userHasAuthenticated(false);
  }

  return (
    isAuthenticating ? null : (
      <div className="App container py-3">
        <Navbar collapseOnSelect bg="light" expand="md" className="mb-3">
          <LinkContainer to="/">
            <Navbar.Brand className="font-weight-bold text-muted">
              Scratch
            </Navbar.Brand>
          </LinkContainer>
          <Navbar.Toggle />
          <Navbar.Collapse className="justify-content-end">
            <Nav activeKey={window.location.pathname}>
              {isAuthenticated ? (
                <Nav.Link onClick={handleLogout}>로그아웃</Nav.Link>
              ) : (
                <>
                  <LinkContainer to="/signup">
                    <Nav.Link>회원가입</Nav.Link>
                  </LinkContainer>
                  <LinkContainer to="/login">
                    <Nav.Link>로그인</Nav.Link>
                  </LinkContainer>
                </>
              )}
            </Nav>
          </Navbar.Collapse>
        </Navbar>
        <AppContext.Provider value={{ isAuthenticated, userHasAuthenticated }}>
          <Routes />
        </AppContext.Provider>
      </div>
    )
  );
}

export default App;

위에 코드에서 UseEffect Hook을 사용하여 앱이 처음 로드될 때만 사용자의 인증 상태를 확인하고 있다.

useEffect Hook의 두 번째 인자에 빈 배열을 전달하면 앱이 로드됐을 때 한 번만 실행하게 해주는 역할을 하게 된다.

상태가 준비되면 렌더링

사용자 세션을 로드하는 것은 비동기식이므로 앱이 처음 로드될 때 상태가 변경되지 않게 위해 메모 애플리케이션 페이지의 렌더링을 보류하도록 설정하자.

src/App.tsx 파일에서 isAuthenticating 상태값을 기반으로 메모 애플리케이션을 렌더링하도록 변경한다 .

return (
  isAuthenticating ? null : (
    <div className="App container py-3">
      <Navbar collapseOnSelect bg="light" expand="md" className="mb-3">
        <LinkContainer to="/">
          <Navbar.Brand className="font-weight-bold text-muted">
            Scratch
          </Navbar.Brand>
        </LinkContainer>
        <Navbar.Toggle />
        <Navbar.Collapse className="justify-content-end">
          <Nav activeKey={window.location.pathname}>
            {isAuthenticated ? (
              <Nav.Link onClick={handleLogout}>로그아웃</Nav.Link>
            ) : (
              <>
                <LinkContainer to="/signup">
                  <Nav.Link>회원가입</Nav.Link>
                </LinkContainer>
                <LinkContainer to="/login">
                  <Nav.Link>로그인</Nav.Link>
                </LinkContainer>
              </>
            )}
          </Nav>
        </Navbar.Collapse>
      </Navbar>
      <AppContext.Provider value={{ isAuthenticated, userHasAuthenticated }}>
        <Routes />
      </AppContext.Provider>
    </div>
  )
);
이제 로그인 후 새로고침을 하여도 아래처럼 로그인이 유지되는 것을 확인할 수 있다.

로그아웃 시 세션 지우기

페이지를 처음 로드할 때 브라우저의 Local Storage에서 사용자 세션을 로드하여 실제로 다시 로그인을 하도록 하고있다.

하지만 실제 로그아웃할 때 이 세션정보를 지울 수 있도록 해보자.

src/App.tsx 파일에서 handleLogout() 함수에 아래처럼 구성해주자.

async function handleLogout() {
  await Auth.signOut();

  userHasAuthenticated(false);
}

로그인 및 로그아웃 시 리다이렉션

로그인 구성을 완료하려면 두 가지 작업이 더 필요하다.

  1. 로그인 후 사용자를 홈페이지로 리다이렉션
  2. 로그아웃한 후 로그인 페이지로 리다이렉션

React Router에서 제공하는 useHistory Hook을 사용하여 브라우저의 History API 를 이용해보자.

src/Login.tsx 파일에서 코드를 아래처럼 수정해주자.

import { useHistory } from "react-router-dom"; // 상단에 추가

const history = useHistory(); // function Login() 밑에 선언

// history.push("/") 내용 추가
async function handleSubmit(event: any) {
  event.preventDefault();

  try {
    await Auth.signIn(email, password);
    userHasAuthenticated(true);
    history.push("/"); // 로그인 시 이동
  } catch (e: unknown) {
    alert((e as Error).message);
  }
}

다음으로는 src/App.tsx 파일에서 코드를 아래처럼 수정해주자.

import { useHistory } from "react-router-dom"; // 상단에 추가

const history = useHistory(); // function App() 바로 아래 추가
  
// 아래처럼 수정  
async function handleLogout() {
  await Auth.signOut();

  userHasAuthenticated(false);

  history.push("/login"); // 로그아웃 시 로그인페이지로 이동
}

 

로그인 구성을 완료하고 다음에는 로그인 하는데 시간이 소요되기 때문에 진행 중이라는 피드백을 사용자에게 제공해보자

isLoading 플래그 사용

src/containers/Login.tsx 파일에서 로그인 진행중을 표시하기위해 관련코드를 구성해보자.

// const [email, setEmail] = useState(""); 위에 구성
const [isLoading, setIsLoading] = useState(false);

// 아래처럼 수정
async function handleSubmit(event: any) {
  event.preventDefault();

  setIsLoading(true);

  try {
    await Auth.signIn(email, password);
    userHasAuthenticated(true);
    history.push("/");
  } catch (e: unknown) {
    alert((e as Error).message);
    setIsLoading(false);
  }
}

로더 버튼 생성

지금 사용중인 버튼에 isLoading 데이터와 연결하여 로딩을 구현할 수도 있겠지만 여러곳에서 로딩버튼이 필요할 수 있으니 따로 로더 버튼을 컴포넌트로 생성하자.

src/components/LoaderButton.tsx 폴더를 만들고 파일을 생성하여 아래처럼 코드를 구성하자.

import React from "react";
import Button from "react-bootstrap/Button";
import { BsArrowRepeat } from "react-icons/bs";
import "./LoaderButton.css";

export interface ILoaderButtonProp {
    isLoading: boolean
    className: string
    disabled: boolean
    [x: string]: any
}

export default function LoaderButton({
  isLoading,
  className = "",
  disabled = false,
  ...props
}: ILoaderButtonProp) {
  return (
    <Button
      disabled={disabled || isLoading}
      className={`LoaderButton ${className}`}
      {...props}
    >
      {isLoading && <BsArrowRepeat className="spinning" />}
      {props.children}
    </Button>
  );
}

위에 LoaderButton Component 에 간단히 살펴보자.

  • isLoading, disabled 두 속성을 사용하여 버튼의 활성화 유무를 결정할 수 있다.
  • className 속성은 내부적으로 사용중인 class가 재정의되지 않게 설정하였다.

src/components/LoaderButton.css 파일을 생성해주고 아래처럼 스타일 구성을 해주자.

.LoaderButton .spinning {
  margin-right: 7px;
  top: 2px;
  animation: spin 1s infinite linear;
}

@keyframes spin {
  from {
    transform: scale(1) rotate(0deg);
  }
  to {
    transform: scale(1) rotate(360deg);
  }
}

src/containers/Login.tsx 파일에서 LoaderButton 컴포넌트를 구성해주자.

import Button from "react-bootstrap/Button"; // 제거
import LoaderButton from "../components/LoaderButton"; // 새로 추가

<Button block size="lg" type="submit" disabled={!validateForm()}>
  로그인
</Button>

// 위에 버튼을 아래로 교체
<LoaderButton
  block
  size="lg"
  type="submit"
  isLoading={isLoading}
  disabled={!validateForm()}
  className={""}
  >
  로그인
</LoaderButton>

오류 처리

오류 처리를 단순하게 유지하기 위해 오류관련 처리를 한곳에서 하기 위해 모듈화를 진행해보자.

src/lib/errorLib.tsx 파일을 생성하고 아래처럼 코드를 구성해주자.

export function onError(error: unknown) {
    let message = (error as Error).toString();
  
    // Auth errors
    if (!(error instanceof Error) && (error as Error).message) {
      message = (error as Error).message;
    }
  
    alert(message);
  }

이제 오류를 표시하는 페이지들을 위에 함수를 사용할 수 있도록 변경해주자

src/containers/Login.tsx 파일에서 수정해주자.

import { onError } from "../lib/errorLib"; // 상단에 추가

// catch 영역의 alert을 아래처럼 교체
async function handleSubmit(event: any) {
    event.preventDefault();
  
    setIsLoading(true);

    try {
      await Auth.signIn(email, password);
      userHasAuthenticated(true);
      history.push("/");
    } catch (e: unknown) {
      onError(e);
      setIsLoading(false);
    }
  }

src/App.tsx 파일에서 수정해주자.

import { onError } from "./lib/errorLib"; // 상단에 추가

// catch 스코프 내에 alert 부분 변경
async function onLoad() {
    try {
      await Auth.currentSession();
      userHasAuthenticated(true);
    }
    catch(e) {
      if (e !== 'No current user') {
        onError(e);
      }
    }
  
    setIsAuthenticating(false);
  }
이제 마지막으로 Github에 작성한 소스를 올려주자

[React] 서버리스 메모앱-3

  • AWS Coginto 연결
  • 로그인, 로그아웃
  • 세션 유지
  • 에러 공통처리

이번 문서에서는 위와 같이 로그인에 관련된 부분을 구성하였습니다.

다음 파트부터는 React Custome Hook, 회원가입 구성, 메모관련 구성에 대해 다루게 되며 [React] 서버리스 메모앱-4 파트에서 정리하도록 하겠습니다.

'IT > React' 카테고리의 다른 글

[React] 서버리스 메모앱-5  (0) 2022.05.10
[React] 서버리스 메모앱-4  (0) 2022.05.07
[React] 서버리스 메모앱-2  (3) 2022.04.18
[React] 서버리스 메모앱-1  (1) 2022.04.17