ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 1월 11~12일 - 웹 사이트 만들기(9) - reactstrap, graphql 요청, jwt 토큰
    공부/웹 사이트 개발(끝) 2021. 1. 11. 16:09

    회원가입, 로그인 UI 만들기

    모듈 설치

    npm i -S bootstrap reactstrap

    npm i -D @types/bootstrap @types/reactstrap

    reactstrap은 이쁘장한? UI들이 모여있는 라이브러이이다.

     

    Next.js에 reactstrap적용

    9.5.4 버전부터는 좀 더 간편하게 모듈로부터 스타일을 임포트할 수 있게 되었다.

     

    일단 /page 폴더에 _app.tsx 파일을 만들어주자. _App은 모든 페이지에 적용되는 로직을 추가할 수 있는 코드이다. (Next.js uses the App component to initialize pages. You can override it and control the page initialization.) _app에 대한 자세한 것은 (nextjs.org/docs/advanced-features/custom-app)

    import "bootstrap/dist/css/bootstrap.css";
    import type { AppProps /*, AppContext */ } from "next/app";
    
    export default function MyApp({ Component, pageProps }: AppProps) {
      return <Component {...pageProps} />;
    }
    

     

    css 관련해서 더 자세히 알고 싶으면 아래 글 참고

    nextjs.org/docs/basic-features/built-in-css-support#import-styles-from-node_modules

     

    Basic Features: Built-in CSS Support | Next.js

    Next.js supports including CSS files as Global CSS or CSS Modules, using `styled-jsx` for CSS-in-JS, or any other CSS-in-JS solution! Learn more here.

    nextjs.org

    UI 만들고 api server에 요청하기

    코드를 조금 설명하면

    onChange 함수의 경우 as Pick<IState, "email" | "password> 코드가 없으면 [x]: string이라는 놈이 IState에 없다는 린트에러가 발생한다.

    login 함수를 보면 쉽게 api 요청을 할 수 있다. 그리고 우선 로그인 시 jwt 토큰을 반환하는 코드까진 구현했는데 이는 뒤에서 설명할 예정이다.

    import Link from "next/link";
    import React from "react";
    import axios from "axios";
    import { InputGroup, Input, Button } from "reactstrap";
    
    interface IState {
      email: string;
      password: string;
    }
    
    class Index extends React.Component<undefined, IState> {
      public constructor(props: any) {
        super(props);
        this.state = {
          email: "",
          password: "",
        };
      }
    
      render() {
        const { email, password } = this.state;
    
        return (
          <div>
            <div>
              <InputGroup style={{ width: "200px" }}>
                <Input
                  type="text"
                  value={email}
                  onChange={this.onChange("email")}
                  style={{ margin: "4px" }}
                />
                <Input
                  type="text"
                  value={password}
                  onChange={this.onChange("password")}
                  style={{ margin: "4px" }}
                />
              </InputGroup>
    
              <Button onClick={this.login} style={{ margin: "4px" }}>
                로그인
              </Button>
              <Link href="/signup">
                <Button onClick={this.login} style={{ margin: "4px" }}>
                  회원가입
                </Button>
              </Link>
            </div>
          </div>
        );
      }
    
      private onChange = (keyName: string) => (
        e: React.ChangeEvent<HTMLInputElement>,
      ) => {
        this.setState({
          [keyName]: e.target.value,
        } as Pick<IState, "email" | "password">);
      };
    
      private login = async () => {
        const result = await axios.post("http://localhost:4000/graphql", {
          query: ` query login($email: String!, $password: String!){
              login(email: $email, password: $password)
          }
          `,
          variables: {
            email: this.state.email,
            password: this.state.password,
          },
        });
    
        if (result) {
          console.log(result.data.data.login);
    
          alert("로그인 성공! 토큰값 받았어용");
        }
      };
    }
    
    export default Index;
    

    회원가입
    로그인

    UI 디자인이 안 이쁜 것은 나중에 다 할 예정...

     

    JWT

    json web token의 약자이다.

     

    왜 jwt가 필요할까

    api-server에 request를 할 때 토큰값이 없으면

    - 누군가 request를 임의로 요청해 무한요청하는 것이 가능하다.

    - token 값에 따라서 api에 대한 접근을 제어할 수 있다.

     

    그래서 우리는 프론트엔드에서 api 요청을 할 때 token값을 백엔드로 보내야하고, 

    백엔드에서는 로그인을 했을 때 token 값을 프론트엔드로 전달해 주고, 요청이 들어올 때마다 token을 확인해 주어야 한다.

     

    1. login에 성공했을 때 token 값 반환하기

    api-server에서 아래 모듈을 설치해 준다.

    npm i -S jsonwebtoken

    npm i -D @types/jsonwebtoken

     

    JWT_SECRET_KEY는 토큰을 만들 때 사용하는key 값이다. 이 값이 있으면 api-server에 요청할 때 유요한 토큰을 만들어낼 수 있기 때문에  들켜서는 안되고, 사실 아래처럼 상수로 선언해서 써도 안된다고 한다. (그건 나중에 하기로)

     

    이렇게 하면 login후에 결과물로 token 값을 얻을 수 있다.

     

    2. 반환받은 token 값 cookie에 저장하기

    일단, 쿠키에 토큰값을 저장하는 이유는 매번 요청을 통해 token값을 가져오면 리소스낭비가 생기기 때문이다. 

     

    cookie 값을 셋팅할 수 있는 모듈을 프론트엔드(user)에 설치한다.

    npm i -S universal-cookie

    npm i -D @types/universal-cookie

     

    아래 보이는 긴 영어가 token값이다.

     

     

    받은 토큰값을 사용해서 cookie 셋팅을 한다.

    plannerRPG는 이름, result.data.data.login은 value, maxAge는 유통기한을 의미한다. (저 숫자는 귀찮아서 대충 때리박았음)

    private login = async () => {
        const result = await axios.post("http://localhost:4000/graphql", {
          query: ` query login($email: String!, $password: String!){
              login(email: $email, password: $password)
          }
          `,
          variables: {
            email: this.state.email,
            password: this.state.password,
          },
        });
    
        if (result) {
          console.log(result.data.data.login);
          const cookies = new Cookies();
          cookies.set("plannerRPG", result.data.data.login, {
            maxAge: 1000 * 60 * 100,
          });
    
          alert("로그인 성공! 토큰값 받았어용");
        }
      };

    이렇게 하고 개발자 툴을 켜서 application -> Cookies -> http://localhost:3000을 들어가보면 쿠키를 확인할 수 있다.

     

    3. 미들웨어 등록하기??

    이 행동을 아마도 어플리케이션 레벨의 미들웨어를 등록한다고 하는 것 같다.(참고 - expressjs.com/ko/guide/using-middleware.html)

     

    app.use() 함수 첫 번째 인자에 함수가 들어가면 이 앱이 요청을 받을 때마다 해당 함수를 실행하게 되며, 첫 번째 인자에 경로가 있고 두 번째 인자에 함수가 있으면 첫 번째 경로의 요청에 대해서만 함수가 실행된다는 듯 하다.

     

    요청이 ~ 올 때 마다~~가 있는 코드를 저 사이에 넣고, 프론트엔드에서 요청을 날리면 아래처럼 텍스트가 보이는 것을 확인할 수 있다.

    그럼 프론트엔드에서 요청을 보낼 때 요청헤더에다가 token값을 넣어서 보내보자.

     

    사실 아래 코드가 가능한 이유는 이미 로그인을 통해 token값을 쿠키에 셋팅한 적이 있기 때문이다. 이는 프론트엔드에서 글로벌한 로직을 통해 로그인 상태가 아니고 + 쿠키 값에 유효한 유저가 아닌 것이 확인되면 게스트 토큰을 셋팅하는 로직이 필요하다.(이는 나중에)

     

    그리고 login api에서는 id와 userEmail을 통해서 토큰을 만들어줬기 때문에 프론트엔드에서 보내는 토큰을 디코딩해도 권한관련 정보가 없기 때문에 권한에 따라(로그인 유저냐, 아니냐) api 처리를 따로 할 수가 없다. 근데 이는 다음에 하기로 하자.

     

    headers 부분에 코드를 추가해서 요청을 보내게 되는데, Bearer라는 string은 왜 필요한건가 했는데 이렇게 해야 암호화가 되어 요청이 날라간다고 한다. (자세한 것은 swagger.io/docs/specification/authentication/bearer-authentication/)

    private login = async () => {
        const cookies = new Cookies();
        const headers = {
          "Content-Type": "application/json",
          Authorization: `Bearer ${cookies.get("plannerRPG")}`,
        };
    
        const result = await axios.post(
          "http://localhost:4000/graphql",
          {
            query: ` query login($email: String!, $password: String!){
              login(email: $email, password: $password)
          }
          `,
            variables: {
              email: this.state.email,
              password: this.state.password,
            },
          },
          {
            headers,
          },
        );
    
        if (result) {
          cookies.set("plannerRPG", result.data.data.login, {
            maxAge: 1000 * 60 * 100,
          });
    
          alert("로그인 성공! 토큰값 받았어용");
        }
      };

     

    이렇게 백엔드에 요청한 것을 백엔드에서 확인해 보자.

    이제 토큰값이 유효한지 확인하는 로직으로 바꿔보자.

    aplit이 들어간 이유는 Bearer을 잘라줘야하기 때문이다. (뭔가 방법이 있을 거 같은데 이것도 나중애)

    app.use((req, res, next) => {
      jwt.verify(
        req.headers.authorization!.split(" ")[1]!,
        JWT_SECRET_KEY,
        (err, decoded) => {
          if (err) {
            throw Error("실패!");
          } else {
            next();
          }
        },
      );
    });

    테스트삼아 쿠키의 value 맨 앞자리를 아무거나로 바꾸고 로그인 요청을 해보니 실패했다. 그리고 원상복귀 후 요청을 하니 정상적으로 작동하는 것까지 확인할 수 있었다.


    추가로 할일

    - 중요 상수들 털리지?않게 관리하기

    - 토큰별 권한 나누기

    - 로그인 안 했을 때 + 쿠키 없을 때 게스트 토큰 만들어주기

     

    내일 할일

    - 어디에 배포할지 결정하기

     

     

Designed by Tistory.