윤스페이스 : 일상을 기록합니다.

이 포스팅은 https://blog.strapi.io/strapi-next-setup/ 에 있는 포스팅을 번역한 것 입니다.

오역이나 의역을 지적해주시면 확인 후 수정하겠습니다.


이 포스팅은 'Next.JS(React)와 GraphQL로 Deliveroo 클론하기'의 연재작 입니다.

Next.JS(React)와 GraphQL로 Deliveroo 클론하기 | 01. 🏗️ 기본 셋업

Next.JS(React)와 GraphQL로 Deliveroo 클론하기 | 02. 🏠 음식점 리스트 만들기

Next.JS(React)와 GraphQL로 Deliveroo 클론하기 | 03. 🍔 음식 리스트 만들기

Next.JS(React)와 GraphQL로 Deliveroo 클론하기 | 04. 🔐 회원 인증

Next.JS(React)와 GraphQL로 Deliveroo 클론하기 | 05. 🛒 장바구니 만들기

Next.JS(React)와 GraphQL로 Deliveroo 클론하기 | 06. 💵 주문 및 결제하기

Next.JS(React)와 GraphQL로 Deliveroo 클론하기 | 07. 🚀 보너스 : 배포하기


🔐 인증

튜토리얼에서 만들고 있는 Next.JS 웹앱에서 인증으로 Strapi SDK(Software Development Kit)를 이용할 수 있습니다. Strapi 서버에서는 JWT 토큰을 리턴하고 있습니다. Strapi 인증과 관련된 문서는  (https://strapi.io/documentation/1.xx/users.html)를 참고해주세요. 

 

인증 요청

인증을 위해 두개의 요소를 사용하려 합니다. 

프로젝트 루트 폴더에 새로운 hocs 디렉터리를 생성합니다.

 

hocs 디렉터리 안에 defaultPage.js를 아래와 같은 코드로 생성해주세요.

import React from "react";
import Router from "next/router";

import { getUserFromServerCookie, getUserFromLocalCookie } from "../lib/auth";

export default Page =>
  class DefaultPage extends React.Component {
    static async getInitialProps({ req }) {
      const loggedUser = process.browser
        ? getUserFromLocalCookie()
        : getUserFromServerCookie(req);
      const pageProps = Page.getInitialProps && Page.getInitialProps(req);
      console.log("is authenticated");
      console.log(loggedUser);
      let path = req ? req.pathname : "";
      path = "";
      return {
        ...pageProps,
        loggedUser,
        currentUrl: path,
        isAuthenticated: !!loggedUser
      };
    }

    logout = eve => {
      if (eve.key === "logout") {
        Router.push(`/?logout=${eve.newValue}`);
      }
    };

    componentDidMount() {
      window.addEventListener("storage", this.logout, false);
    }

    componentWillUnmount() {
      window.removeEventListener("storage", this.logout, false);
    }

    render() {
      return <Page {...this.props} />;
    }
  };

 

hocs 디렉터리 안에 securePage.js를 아래와 같은 코드로 생성해주세요. 

import React from "react";
import PropTypes from "prop-types";

import defaultPage from "./defaultPage";

const securePageHoc = Page =>
  class SecurePage extends React.Component {
    static propTypes = {
      isAuthenticated: PropTypes.bool.isRequired
    };

    static getInitialProps(ctx) {
      return Page.getInitialProps && Page.getInitialProps(ctx);
    }

    render() {
      const { isAuthenticated } = this.props;
      return isAuthenticated ? <Page {...this.props} /> : "Not Authorized";
    }
  };

export default Page => defaultPage(securePageHoc(Page));

 

인증 함수를 완벽하게 구현하기 위해 lib 디렉터리에 auth.js 파일을 아래와 같은 코드로 생성해주세요.

import jwtDecode from "jwt-decode";
import Cookies from "js-cookie";
import Strapi from "strapi-sdk-javascript/build/main";

import Router from "next/router";

const apiUrl = process.env.API_URL || "http://localhost:1337";
const strapi = new Strapi(apiUrl);

export const strapiRegister = (username, email, password) => {
  if (!process.browser) {
    return undefined;
  }
  strapi.register(username, email, password).then(res => {
    setToken(res);
  });
  return Promise.resolve();
};
//use strapi to get a JWT and token object, save
//to approriate cookei for future requests
export const strapiLogin = (email, password) => {
  if (!process.browser) {
    return;
  }
  // Get a token
  strapi.login(email, password).then(res => {
    setToken(res);
  });
  return Promise.resolve();
};

export const setToken = token => {
  if (!process.browser) {
    return;
  }
  Cookies.set("username", token.user.username);
  Cookies.set("jwt", token.jwt);

  if (Cookies.get("username")) {
    Router.push("/");
  }
};

export const unsetToken = () => {
  if (!process.browser) {
    return;
  }
  Cookies.remove("jwt");
  Cookies.remove("username");
  Cookies.remove("cart");

  // to support logging out from all windows
  window.localStorage.setItem("logout", Date.now());
  Router.push("/");
};

export const getUserFromServerCookie = req => {
  if (!req.headers.cookie || "") {
    return undefined;
  }

  let username = req.headers.cookie
    .split(";")
    .find(user => user.trim().startsWith("username="));
  if (username) {
    username = username.split("=")[1];
  }

  const jwtCookie = req.headers.cookie
    .split(";")
    .find(c => c.trim().startsWith("jwt="));
  if (!jwtCookie) {
    return undefined;
  }
  const jwt = jwtCookie.split("=")[1];
  return jwtDecode(jwt), username;
};

export const getUserFromLocalCookie = () => {
  return Cookies.get("username");
};

//these will be used if you expand to a provider such as Auth0
const getQueryParams = () => {
  const params = {};
  window.location.href.replace(
    /([^(?|#)=&]+)(=([^&]*))?/g,
    ($0, $1, $2, $3) => {
      params[$1] = $3;
    }
  );
  return params;
};
export const extractInfoFromHash = () => {
  if (!process.browser) {
    return undefined;
  }
  const { id_token, state } = getQueryParams();
  return { token: id_token, secret: state };
};

 

이후 jwt-decode, js-cookie, strapi-sdk-javascript 패키지를 설치해줍니다.

> npm install jwt-decode js-cookie strapi-sdk-javascript 

 

쿠키를 사용하는 이유  🍪

대부분의 경우 웹 앱은 JSON WEB TOKEN(JWT)를 로컬 저장소에 저장합니다. Strapi JavaScript SDK가 기본적으로 쿠키로 저장하는 작업입니다. 이번 튜토리얼에서 NavBar에 사용자 이름을 표시하려 토큰의 저장이 필요합니다. 로컬 스토리지에 저장할 수는 있지만 Next는 로컬 스토리지에 접근이 어렵기에 쿠키에 저장해야 합니다.

 

회원가입

회원가입을 위해 Strapi SDK에 사용자 이름, 이메일 및 암호를 전달해줘야 합니다. 회원가입 페이지에서 auth.js 파일로 가입시킨 후 토큰을 캐시에 저장하려 합니다. 경로에 맞게 파일을 수정해주세요.

 

/pages/signup.js

import React from "react";
import { strapiRegister } from "../lib/auth";

import Router from "next/router";
import {
  Container,
  Row,
  Col,
  Button,
  Form,
  FormGroup,
  Label,
  Input,
  FormText
} from "reactstrap";

class SignUp extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      data: {
        email: "",
        username: "",
        password: ""
      },
      loading: false,
      error: ""
    };
  }

  onChange(propertyName, event) {
    const { data } = this.state;
    data[propertyName] = event.target.value;
    this.setState({ data });
  }
  onSubmit() {
    const {
      data: { email, username, password }
    } = this.state;
    this.setState({ loading: true });

    strapiRegister(username, email, password)
      .then(() => this.setState({ loading: false }))
      .catch(error => this.setState({ error: error }));
  }

  render() {
    const { error } = this.state;
    return (
      <Container>
        <Row>
          <Col sm="12" md={{ size: 5, offset: 3 }}>
            <div className="paper">
              <div className="header">
                <img src="https://strapi.io/assets/images/logo.png" />
              </div>
              <section className="wrapper">
                <div className="notification">{error}</div>
                <Form>
                  <FormGroup>
                    <Label>Username:</Label>
                    <Input
                      onChange={this.onChange.bind(this, "username")}
                      type="text"
                      name="username"
                      style={{ height: 50, fontSize: "1.2em" }}
                    />
                  </FormGroup>
                  <FormGroup>
                    <Label>Email:</Label>
                    <Input
                      onChange={this.onChange.bind(this, "email")}
                      type="email"
                      name="email"
                      style={{ height: 50, fontSize: "1.2em" }}
                    />
                  </FormGroup>
                  <FormGroup style={{ marginBottom: 30 }}>
                    <Label>Password:</Label>
                    <Input
                      onChange={this.onChange.bind(this, "password")}
                      type="password"
                      name="password"
                      style={{ height: 50, fontSize: "1.2em" }}
                    />
                  </FormGroup>
                  <FormGroup>
                    <span>
                      <a href="">
                        <small>Forgot Password?</small>
                      </a>
                    </span>
                    <Button
                      style={{ float: "right", width: 120 }}
                      color="primary"
                      onClick={this.onSubmit.bind(this)}
                    >
                      Submit
                    </Button>
                  </FormGroup>
                </Form>
              </section>
            </div>
          </Col>
        </Row>
        <style jsx>
          {`
            .paper {
              border: 1px solid lightgray;
              box-shadow: 0px 1px 3px 0px rgba(0, 0, 0, 0.2),
                0px 1px 1px 0px rgba(0, 0, 0, 0.14),
                0px 2px 1px -1px rgba(0, 0, 0, 0.12);
              height: 540px;
              border-radius: 6px;
              margin-top: 90px;
            }
            .notification {
              color: #ab003c;
            }
            .header {
              width: 100%;
              height: 120px;
              background-color: #2196f3;
              margin-bottom: 30px;
              border-radius-top: 6px;
            }
            .wrapper {
              padding: 10px 30px 20px 30px !important;
            }
            a {
              color: blue !important;
            }
            img {
              margin: 15px 30px 10px 50px;
            }
          `}
        </style>
      </Container>
    );
  }
}
export default SignUp;

 

로그아웃

처음 포스팅에서 만들었던 layout.js에서 사용자 인증을 확인하고 로그아웃 버튼을 만들려고 합니다. 로그 아웃 버튼은 unsetToken 기능을 호출하여 쿠키를 삭제하고 홈페이지로 이동하도록 설정합니다. layout.js 파일을 아래와 같이 수정해주세요.

 

import React from "react";
import Head from "next/head";
import Link from "next/link";
import { unsetToken } from "../lib/auth";
import { Container, Nav, NavItem } from "reactstrap";
import defaultPage from "../hocs/defaultPage";
import Cookie from "js-cookie";

class Layout extends React.Component {
  constructor(props) {
    super(props);
  }
  static async getInitialProps({ req }) {
    let pageProps = {};
    if (Component.getInitialProps) {
      pageProps = await Component.getInitialProps(ctx);
    }

    return { pageProps, isAuthenticated };
  }
  render() {
    const { isAuthenticated, children } = this.props;
    const title = "Welcome to Nextjs";
    return (
      <div>
        <Head>
          <title>{title}</title>
          <meta charSet="utf-8" />
          <meta
            name="viewport"
            content="initial-scale=1.0, width=device-width"
          />
          <link
            rel="stylesheet"
            href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css"
            integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm"
            crossOrigin="anonymous"
          />
          <script src="https://js.stripe.com/v3" />
        </Head>
        <header>
          <Nav className="navbar navbar-dark bg-dark">
            <NavItem>
              <Link href="/">
                <a className="navbar-brand">Home</a>
              </Link>
            </NavItem>
            {isAuthenticated ? (
              <>
                <NavItem className="ml-auto">
                  <span style={{ color: "white", marginRight: 30 }}>
                    {this.props.loggedUser}
                  </span>
                </NavItem>
                <NavItem>
                  <Link href="/">
                    <a className="logout" onClick={unsetToken}>
                      Logout
                    </a>
                  </Link>
                </NavItem>
              </>
            ) : (
              <>
                <NavItem className="ml-auto">
                  <Link href="/signin">
                    <a className="nav-link">Sign In</a>
                  </Link>
                </NavItem>

                <NavItem>
                  <Link href="/signup">
                    <a className="nav-link"> Sign Up</a>
                  </Link>
                </NavItem>
              </>
            )}
          </Nav>
        </header>
        <Container>{children}</Container>
        {/* <footer className="footer">
          {"Strapi footer"}
          <style jsx>
            {`
              .footer {
                position: absolute;
                bottom: 0;
                width: 100%;
                height: 60px;
                line-height: 60px;
                background-color: #f5f5f5;
              }
              a:hover {
                cursor: pointer;
                color: yellow;
              }
            `}
          </style>
        </footer> */}
      </div>
    );
  }
}

export default defaultPage(Layout);

 

로그인

회원가입 페이지와 마찬가지로 로그인 페이지에서 Strapi SDK를 사용하여 로그인하고 나중에 사용할 수 있도록 아이디과 JWT 쿠키를 캐시에 남깁니다. pages/signin.js 파일을 아래와 같이 수정해주세요.

import React from "react";

import defaultPage from "../hocs/defaultPage";
import { strapiLogin } from "../lib/auth";

import Router from "next/router";
import {
  Container,
  Row,
  Col,
  Button,
  Form,
  FormGroup,
  Label,
  Input,
  FormText
} from "reactstrap";
import Cookies from "js-cookie";

class SignIn extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      data: {
        email: "",
        password: ""
      },
      loading: false,
      error: ""
    };
  }
  componentDidMount() {
    if (this.props.isAuthenticated) {
      Router.push("/"); // redirect if you're already logged in
    }
  }

  onChange(propertyName, event) {
    const { data } = this.state;
    data[propertyName] = event.target.value;
    this.setState({ data });
  }
  onSubmit() {
    const {
      data: { email, username, password }
    } = this.state;
    const { context } = this.props;

    this.setState({ loading: true });

    strapiLogin(email, password).then(() => console.log(Cookies.get("user")));
  }
  render() {
    const { error } = this.state;
    return (
      <Container>
        <Row>
          <Col sm="12" md={{ size: 5, offset: 3 }}>
            <div className="paper">
              <div className="header">
                <img src="https://strapi.io/assets/images/logo.png" />
              </div>
              <section className="wrapper">
                <div className="notification">{error}</div>
                <Form>
                  <FormGroup>
                    <Label>Email:</Label>
                    <Input
                      onChange={this.onChange.bind(this, "email")}
                      type="email"
                      name="email"
                      style={{ height: 50, fontSize: "1.2em" }}
                    />
                  </FormGroup>
                  <FormGroup style={{ marginBottom: 30 }}>
                    <Label>Password:</Label>
                    <Input
                      onChange={this.onChange.bind(this, "password")}
                      type="password"
                      name="password"
                      style={{ height: 50, fontSize: "1.2em" }}
                    />
                  </FormGroup>

                  <FormGroup>
                    <span>
                      <a href="">
                        <small>Forgot Password?</small>
                      </a>
                    </span>
                    <Button
                      style={{ float: "right", width: 120 }}
                      color="primary"
                      onClick={this.onSubmit.bind(this)}
                    >
                      Submit
                    </Button>
                  </FormGroup>
                </Form>
              </section>
            </div>
          </Col>
        </Row>
        <style jsx>
          {`
            .paper {
              border: 1px solid lightgray;
              box-shadow: 0px 1px 3px 0px rgba(0, 0, 0, 0.2),
                0px 1px 1px 0px rgba(0, 0, 0, 0.14),
                0px 2px 1px -1px rgba(0, 0, 0, 0.12);
              height: 440px;
              border-radius: 6px;
              margin-top: 90px;
            }
            .notification {
              color: #ab003c;
            }
            .header {
              width: 100%;
              height: 120px;
              background-color: #2196f3;
              margin-bottom: 30px;
              border-radius-top: 6px;
            }
            .wrapper {
              padding: 10px 30px 20px 30px !important;
            }
            a {
              color: blue !important;
            }
            img {
              margin: 15px 30px 10px 50px;
            }
          `}
        </style>
      </Container>
    );
  }
}
export default SignIn;

다음에는 장바구니를 만드는 방법을 알아볼 예정입니다.

 

질문은 댓글로 남겨주시면 최대한 빠른 시간 안에 답변드리겠습니다.

 

포스팅 읽어주셔서 감사합니다 😁