React Apollo Hooks를 사용하여 로그인 기능을 만들었습니다.

소개


react-apollo-hooks 가 편리했기 때문에 SPA 로그인 기능을 만들면서 사용법을 소개합니다.

실제로 작성한 프로젝트는 Github에 올립니다.

이번에도 프로젝트의 병아리는 create-react-app 로 작성합니다.
TypeScript 에서 만들었으므로 명령은 다음과 같습니다.
$ create-react-app sample-login-with-react-apollo-hooks --typescript

※백엔드(API 서버)에 대해서 소개는 포함하고 있지 않습니다.

버전




패키지
버전


typescript
3.5.3

react
16.9.0

react-apollo
3.0.1

react-apollo-hooks
0.5.0

graphql
14.4.2

graphql-tag
2.10.1

react-router-dom
5.0.1


이번에 만드는 기능


  • 로그인에 성공하면 루트 페이지로 리디렉션
  • 백엔드에서받은 토큰은 localStorage에 저장됩니다.

  • 로그인에 실패하면 오류 표시
  • 이미 로그인한 경우 루트 페이지로 리디렉션
  • localStorage 에 저장된 토큰으로 문의하여 로그인되었는지 확인합니다.


  • 화면 준비



    로그인 페이지와 루트 페이지를 준비합니다.

    src/Login/index.tsx
    import React from "react";
    
    const Login = () => {
      return (
        <div>
          <div>
            <label>
              ID
              <br />
              <input type="text" />
            </label>
          </div>
          <div>
            <label>
              PW
              <br />
              <input type="password" />
            </label>
          </div>
          <div>
            <input type="submit" value="ログイン" />
          </div>
        </div>
      );
    };
    
    export default Login;
    

    src/Home/index.tsx
    import React from "react";
    
    const Home = () => {
      return <div>Home</div>;
    };
    
    export default Home;
    

    루트 준비



    src/index.tsx
    import React from "react";
    import { Route, Switch } from "react-router-dom";
    import ReactDOM from "react-dom";
    import { BrowserRouter } from "react-router-dom";
    import * as serviceWorker from "./serviceWorker";
    import Home from "./Home";
    import Login from "./Login";
    
    ReactDOM.render(
      <BrowserRouter>
        <Switch>
          <Route exact path="/" component={Home} />
          <Route path="/login" component={Login} />
        </Switch>
      </BrowserRouter>,
      document.getElementById("root")
    );
    
    // If you want your app to work offline and load faster, you can change
    // unregister() to register() below. Note this comes with some pitfalls.
    // Learn more about service workers: https://bit.ly/CRA-PWA
    serviceWorker.unregister();
    

    http://localhost:3000/login 방문





    http://localhost:3000/ 방문





    페이지와 루트를 준비했습니다.

    react-apollo 준비


    client 를 작성합니다.

    src/index.tsx
     import React from "react";
     import ReactDOM from "react-dom";
     import { BrowserRouter } from "react-router-dom";
     import * as serviceWorker from "./serviceWorker";
    +import { ApolloProvider } from "react-apollo-hooks";
    +import { InMemoryCache } from "apollo-cache-inmemory";
    +import { ApolloClient } from "apollo-client";
    +import { setContext } from "apollo-link-context";
    +import { createHttpLink } from "apollo-link-http";
    +import { Route, Switch } from "react-router-dom";
     import Home from "./Home";
     import Login from "./Login";
    
    +const uri = "http://localhost:5000/graphql";
    +const context = setContext((_, { headers }) => {
    +  return {
    +    headers: { ...headers, token: localStorage.getItem("token") }
    +  };
    +});
    +const link = context.concat(createHttpLink({ uri }));
    +const cache = new InMemoryCache();
    +const client = new ApolloClient({ cache, link });
    
     ReactDOM.render(
       <BrowserRouter>
    +    <ApolloProvider client={client}>
           <Switch>
             <Route exact path="/" component={Home} />
             <Route path="/login" component={Login} />
           </Switch>
    +    </ApolloProvider>
       </BrowserRouter>,
       document.getElementById("root")
     );
    
     // If you want your app to work offline and load faster, you can change
     // unregister() to register() below. Note this comes with some pitfalls.
     // Learn more about service workers: https://bit.ly/CRA-PWA
     serviceWorker.unregister();
    
    uri 에는 백엔드 엔드포인트를 지정하십시오.
    요청 헤더에 tokenlocalStorage 에서 가져와서 설정합니다.
    이번은 Rails + API + GraphQL 의 프로젝트를 5000 번 포트로 기동하고 있었습니다.
    react-apollo-hooks 를 사용할 준비가 되었으므로 즉시 로그인 기능으로 만들어 갑시다.

    로그인 기능



    아래와 같은 Mutation 를 백엔드측에 준비해 둡니다.
    mutation {
      login(input: {loginid: "loginid", password: "password"}) {
        user {
          id
          accessToken {
            token
          }
        }
        result
      }
    }
    

    여기에 로그인 양식에 입력된 값을 포함 요청을 보내고 싶습니다.

    입력 값을 State에 저장



    src/Login/index.tsx
    -import React from "react";
    +import React, { useState } from "react";
    
    ...
     const Login = () => {
    +  const [loginid, setLoginid] = useState("");
    +  const [password, setPassword] = useState("");
    
    ...
    
    -<input type="text" />
    +<input
    +  type="text"
    +  value={loginid}
    +  onChange={e => setLoginid(e.target.value || "")}
    +/>
    
    ...
    
    -<input type="text" />
    +<input
    +  type="password"
    +  value={password}
    +  onChange={e => setPassword(e.target.value || "")}
    +/>
    
    ...
    

    로그인 버튼을 로그인 처리 실행



    src/Login/index.tsx
     import React, { useState } from "react";
    +import { useMutation } from "react-apollo-hooks";
    +import { withRouter } from "react-router";
    +import { History } from "history";
    +import gql from "graphql-tag";
    
    +interface IProps {
    +  history: History;
    +}
    
    +const Login = ({ history }: IProps) => {
    ...
    +  const loginMutation = gql`
    +    mutation login($loginid: String!, $password: String!) {
    +      login(input: { loginid: $loginid, password: $password }) {
    +        user {
    +          accessToken {
    +            token
    +          }
    +        }
    +        result
    +      }
    +    }
    +  `;
    +
    +  const [login] = useMutation(loginMutation, {
    +    update: (_proxy, response) => {
    +      if (response.data.login.result) {
    +        localStorage.setItem(
    +          "token",
    +          response.data.login.user.accessToken.token
    +        );
    +        history.push("/");
    +      } else {
    +        alert("ログイン情報が不正です。");
    +        setLoginid("");
    +        setPassword("");
    +      }
    +    },
    +    variables: { loginid, password }
    +  });
    
       return (
    ...
    -<input type="submit" value="ログイン" />
    +<input type="submit" value="ログイン" onClick={() => login()} />
    ...
    
    -export default Login;
    +export default withRouter(Login);
    

    로그인에 성공( response.data.login.result 하지만 true ) 하면 루트 페이지로 이동해, 실패하면 alert 를 내는 처리가 할 수 있었습니다.

    로그인에 성공하면 응답에서 얻은 토큰을 localStorage로 설정합니다.
    localStorage 에 토큰을 세트하는 처리가 생겼으므로, 이미 로그인된 경우 루트 페이지로 리디렉션하는 처리를 만듭니다.

    로그인한 사용자의 루트 페이지로 자동 리디렉션 처리



    다음과 같은 로그인된 사용자를 얻는 Query 를 백엔드측에 준비해 둡니다.
    {
      loggedUser {
        id
      }
    }
    

    응답에 사용자 정보가 포함되어 있으면 로그인되어 있고 null이면 로그인되지 않은 것으로 판단합니다.

    src/Login/index.tsx
    ...
    -import { withRouter } from "react-router";
    +import { Redirect, withRouter } from "react-router";
    ...
    -import { useMutation } from "react-apollo-hooks";
    +import { useQuery, useMutation } from "react-apollo-hooks";
    ...
    
    +  const loggedUserQuery = gql`
    +    {
    +      loggedUser {
    +        id
    +      }
    +    }
    +  `;
    
       const loginMutation = gql`
    ...
      const [login] = useMutation(loginMutation, {
    ...
    
    +  const { loading, data } = useQuery(loggedUserQuery);
    
    +  if (loading) {
    +    return <div>Now Loading...</div>;
    +  } else if (data.loggedUser) {
    +    return <Redirect to="/" />;
    +  }
    
       const login = () => {
    ...
    

    로그인하고 있는지 문의중에는 로딩 메시지를 표시해, 로그인하고 있으면, 루트 페이지로 리디렉트, 그렇지 않으면, 로그인 페이지를 표시할 수 있게 되었습니다.

    마지막으로



    /graphiql 에서 쿼리를 시도 할 때 프론트 측에서 정의한 쿼리를 복사하기만으로 실행할 수 있기 때문에 기쁩니다.
    ※인수가 있는 경우는 variables 를 화면 좌하의 폼으로 설정할 필요가 있습니다.

    참고문헌


  • react-apollo-hooks - Github
  • React Apollo의 hooks 대응의 베타판이 공개! - Qiita
  • 좋은 웹페이지 즐겨찾기