ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • react + apollo + graphql (영화 앱 만들기)
    graphql/graphql+nodejs(노마드코더) 2020. 3. 7. 17:39

    #1 Apollo GraphQL


     

    기초 셋팅을 보자. 그리고 전체적인 흐름을 파악해보자.

     

    1. 먼저 graphql 서버가 돌아 가고 있어야 한다. 이 서버는 이전에 만들었던  http://localhost:4000로 사용 하겠다.

     

    2.  npm install styled-components react-router-dom apollo-boost @apollo/react-hooks graphql 를 설치해주자.

     

    3.  전체적인 파일 트리는 아래 참고.

     

    4. 각 파일 및 기능 별로 개념을 이해해보자.

     

    >index.js 파일

    import React from "react";
    import ReactDOM from "react-dom";
    import App from "./components/App";
    import { ApolloProvider } from "@apollo/react-hooks";
    import client from "./apollo";
    
    ReactDOM.render(
      <ApolloProvider client={client}>
        <App />
      </ApolloProvider>,
      document.getElementById("root")
    );
    

     기존 <App /> 에 위 아래로, ApolloProvider를 감쌌다. 그리고 client = {client}가 들어가 있다. 

    -ApolloProvider 는 apollo/react-hook 모듈에서 제공,

    기능 : app.js(Home.js, Detail.js) 는 어떤 data를 받아와서 화면에 보여주어야 하는데, 그 data의 출처를 App에 연동해주는 역할임. 여기서 {client}는 아래와 같이 구성 되어 있음. 여기서 uri 부분에 받고자 하는 graphql 서버 주소를 넣으면 된다.

    >apollo.js 파일

    import ApolloClient from "apollo-boost";
    
    const client = new ApolloClient({
        uri: "http://localhost:4000"
    });
    
    export default client;

     

    이어서, App.js를 보자.

    >App.js 파일

    import React from 'react';
    import {HashRouter as Router, Route} from "react-router-dom"
    import Detail from '../routes/Detail';
    import Home from '../routes/Home';
    
    
    function App() {
      return <Router>
        <Route exact path="/" component={Home} />
        <Route path="/:id" component={Detail} />
      </Router>
    
    }
    
    export default App;
    

    HashRouter를 사용하여 Router, 그리고 Route를 각 컴포넌트에 감싸주자. 이때 exact는 정확히 path에 있는 string이 있어야 발동된다는 뜻이다. 이게 없으면 /:id 부분도 /이 포함되어있기 때문에 동시에 발동 된다.

     

    각 컴포넌트를 확인해보자.

    >Home.js 파일

    import React from "react";
    import { gql } from "apollo-boost";
    import { useQuery } from "@apollo/react-hooks";
    import styled from "styled-components";
    import Movie from "../components/Movie";
    
    const GET_MOVIES = gql`
      {
        movies {
          id
          medium_cover_image
        }
      }
    `;
    
    const Container = styled.div`
      display: flex;
      flex-direction: column;
      align-items: center;
      width: 100%;
    `;
    
    const Header = styled.header`
      background-image: linear-gradient(-45deg, #d754ab, #fd723a);
      height: 45vh;
      color: white;
      display: flex;
      flex-direction: column;
      justify-content: center;
      align-items: center;
      width: 100%;
    `;
    
    const Title = styled.h1`
      font-size: 60px;
      font-weight: 600;
      margin-bottom: 20px;
    `;
    
    const Subtitle = styled.h3`
      font-size: 35px;
    `;
    
    const Loading = styled.div`
      font-size: 18px;
      opacity: 0.5;
      font-weight: 500;
      margin-top: 10px;
    `;
    
    const Movies = styled.div`
      display: grid;
      grid-template-columns: repeat(4, 1fr);
      grid-gap: 25px;
      width: 60%;
      position: relative;
      top: -50px;
    `;
    
    // ** optional chaining 미사용
    // export default () => {
    //   const { loading, data } = useQuery(GET_MOVIES);
    //   return (
    //     <Container>
    //       <Header>
    //         <Title>Apollo 2020</Title>
    //         <Subtitle>I love GraphQL</Subtitle>
    //       </Header>
    //       {loading && <Loading>Loading...</Loading>}
    //       {!loading && data.movies && (
    //         <Movies>
    //           {data.movies.map(m => (
    //             <Movie key={m.id} id={m.id} bg={m.medium_cover_image} />
    //           ))}
    //         </Movies>
    //       )}
    //     </Container>
    //   );
    // };
    
    // ** optional chaining 사용
    export default () => {
      const { loading, data } = useQuery(GET_MOVIES);
      return (
        <Container>
          <Header>
            <Title>Apollo 2020</Title>
            <Subtitle>I love GraphQL</Subtitle>
          </Header>
          {loading && <Loading>Loading...</Loading>}
          <Movies>
            {data?.movies?.map(m => (
              <Movie key={m.id} id={m.id} bg={m.medium_cover_image} />
            ))}
          </Movies>
        </Container>
      );
    };
    

     <gql 부분>

    기능 : graphql의 query부분을 담당한다. 이를 useQuery(gql 변수명)에 담는다.

     

    const { loading, data } = useQuery(GET_MOVIES)는 hook 사용법이다. 처음 loading이 발동되고, query요청이 완료 되고 data를 받게 되면 data값이 실행 된다. (리덕스에서 state 및 setState의 역할로 보면 될듯?)

     

    <Movie 컴포넌트 부분>

    위 Home 컴포넌트에서 Movie 서브 컴포넌트가 있다. 확인해보자.

     

     

    >Movie.js 파일

    import React from "react";
    import { Link } from "react-router-dom";
    import styled from "styled-components";
    
    const Container = styled.div`
      height: 380px;
      width: 100%;
      box-shadow: 0 3px 6px rgba(0, 0, 0, 0.16), 0 3px 6px rgba(0, 0, 0, 0.23);
      overflow: hidden;
      border-radius: 7px;
    `;
    
    const Poster = styled.div`
      background-image: url(${props => props.bg});
      height: 100%;
      width: 100%;
      background-size: cover;
      background-position: center center;
    `;
    
    export default ({ id, bg }) => (
      <Container>
        <Link to={`/${id}`}>
          <Poster bg={bg} />
        </Link>
      </Container>
    );
    

    props로 id와 bg를 받아 왔다. 여기서 Link to 역할은 html의 <a href> 역할로 보면 된다.

     

    다시 App.js의 나머지 컴포넌트인 Detail.js 를 보자.

    >Detail.js 파일

    import React from "react";
    import { useParams } from "react-router-dom";
    import { gql } from "apollo-boost";
    import { useQuery } from "@apollo/react-hooks";
    import styled from "styled-components";
    
    const GET_MOVIE = gql`
      query getMovie($id: Int!) {
        movie(id: $id) {
          title
          medium_cover_image
          language
          rating
          description_intro
        }
      }
    `;
    
    const Container = styled.div`
      height: 100vh;
      background-image: linear-gradient(-45deg, #d754ab, #fd723a);
      width: 100%;
      display: flex;
      justify-content: space-around;
      align-items: center;
      color: white;
    `;
    
    const Column = styled.div`
      margin-left: 10px;
      width: 50%;
    `;
    
    const Title = styled.h1`
      font-size: 65px;
      margin-bottom: 15px;
    `;
    
    const Subtitle = styled.h4`
      font-size: 35px;
      margin-bottom: 10px;
    `;
    
    const Description = styled.p`
      font-size: 28px;
    `;
    
    const Poster = styled.div`
      width: 25%;
      height: 60%;
      background-color: transparent;
      background-image: url(${props => props.bg});
      background-size: cover;
      background-position: center center;
    `;
    
    export default () => {
      let { id } = useParams();
      id = parseInt(id);
      //   const { loading, data } = useQuery(GET_MOVIE, {
      //     variables: { id }
      //   });
      //   return (
      //     <Container>
      //       <Column>
      //         <Title>{loading ? "Loading..." : data.movie.title}</Title>
      //         {!loading && data.movie && (
      //           <>
      //             <Subtitle>
      //               {data.movie.language} · {data.movie.rating}
      //             </Subtitle>
      //             <Description>{data.movie.description_intro}</Description>
      //           </>
      //         )}
      //       </Column>
      //       <Poster
      //         bg={data && data.movie ? data.movie.medium_cover_image : ""}
      //       ></Poster>
      //     </Container>
      //   );
      // };
      const { loading, data } = useQuery(GET_MOVIE, {
        variables: { id }
      });
      return (
        <Container>
          <Column>
            <Title>{loading ? "Loading..." : data.movie.title}</Title>
            <Subtitle>
              {data?.movie?.language} · {data?.movie?.rating}
            </Subtitle>
            <Description>{data?.movie?.description_intro}</Description>
          </Column>
          <Poster bg={data?.movie?.medium_cover_image}></Poster>
        </Container>
      );
    };
    

    <gql 부분>

    gql 부분이 이전 Home.js와 구조가 다르다. 인자 유무에 따라 다른 것임.

     

    나머지 부분은 눈으로 보면 이해됨.

     

     

     

     

     

    #2 Local State


     

    Like 버튼을 누르면 해당 버튼이 Unlike로 바뀌게 해보자.

     

    >apollo.js 파일

    import ApolloClient from "apollo-boost";
    
    const client = new ApolloClient({
      uri: "http://localhost:4000",
      resolvers: {
        Movie: {
          isLiked: () => false
        },
        Mutation: {
          likeMovie: (_, { id }, { cache }) => {
            cache.writeData({ id: `Movie:${id}`, data: { isLiked: true } });
          }
        }
      }
    });
    
    export default client;
    

     먼저, resolvers부분을 추가해준다. Movie는 실제 서버에서 받아오는 name이고, 이부분에 isLiked 라는 state를 추가해주는 것이다.  그리고 Mutation 부분은 Like버튼을 클릭하면 cache에 있는 state값을 바꿔주는 로직이다. 여기서 likeMovie라는 네이밍은 여러 파일 안에서 필요한 네임이다. (isLiked처럼 state에서 보여지는것이 아니라, isLiked를 mutation 하기 위한 변수명)

     

    likeMovie 값은 id와, cache 인자값을 받는다.

     

    >Home.js 파일

    const GET_MOVIES = gql`
      {
        movies {
          id
          medium_cover_image
          isLiked @client
        }
      }
    `;

     

    gql부분에 위에 추가한 isLiked를 추가해주자. 그리고 @client를 꼭 써주어야한다. 해당 data를 서버가 아닌 client에서 가져오기 때문이다.

    export default () => {
      const { loading, data } = useQuery(GET_MOVIES);
      return (
        <Container>
          <Header>
            <Title>Apollo 2020</Title>
            <Subtitle>I love GraphQL</Subtitle>
          </Header>
          {loading && <Loading>Loading...</Loading>}
          <Movies>
            {data?.movies?.map(m => (
              <Movie
                key={m.id}
                id={m.id}
                isLiked={m.isLiked}
                bg={m.medium_cover_image}
              />
            ))}
          </Movies>
        </Container>
      );
    };

    isLiked를 props 값으로 Movie 서브컴포넌트에 넘겨준다. (디폴트값은 false)

     

     

    >Movie.js 파일

    const LIKE_MOVIE = gql`
      mutation likeMovie($id: Int!) {
        likeMovie(id: $id) @client
      }
    `;

    likeMovie라는 name은 apollo.js에서 정의한 mutation 부분의 name과 같아야 한다. (해당 값은 client에서 받아왔기 때문에, @client라고 써줘야함)

     

    export default ({ id, bg, isLiked }) => {
      const [likeMovie] = useMutation(LIKE_MOVIE, {
        variables: { id: parseInt(id) }
      });
    
      return (
        <Container>
          <Link to={`/${id}`}>
            <Poster bg={bg} />
          </Link>
          <button onClick={isLiked ? null : likeMovie}>
            {isLiked ? "Unlike" : "Like"}
          </button>
        </Container>
      );
    };

    앞 home.js에서 정의한 isLiked props를 받고, onClick을 하게 되면 isLiked값이 false이면 likeMovie가 실행된다.

    해당 likeMovie함수명은 useMutation의 변수명이다. likeMovie 함수는 useMutation함수를 발동 시켜, LIKE_MOVIE mutation 함수를 발동 시킨다. 그리고 인자값으로 해당 id 값을 넘겨준다. 해당 id값을 받게 되면, apollo.js에서 설정한대로, 해당 id값에 data의 isLiked부분을 true로 바꿔준다.

     

    그리고, 화면에 보여지는 부분의 로직은, isLiked가 true면 Unlike로 표기되고, false면 Like로 표기된다. 따라서, 기본 디폴트 값은 false이기 떄문에 Like로 표기가 되어있고, 버튼을 누르면 Unlike로 변경 된다.

     

    이렇게 하면, unlike를 다시 Like로 바꿀수가 없다. 따라서, 아래와 같이 약간의 코드 수정이 필요하다.

     

    >apollo.js 파일

    import ApolloClient from "apollo-boost";
    
    const client = new ApolloClient({
      uri: "http://localhost:4000",
      resolvers: {
        Movie: {
          isLiked: () => false
        },
        Mutation: {
          toggleLikeMovie: (_, { id, isLiked }, { cache }) => {
            cache.writeData({
              id: `Movie:${id}`,
              data: {
                isLiked: !isLiked
              }
            });
          }
        }
      }
    });
    
    export default client;
    

    toggleLikeMovie 부분 참고, !isLiked를 통해 다른 곳에서의 인자의 값이 isLiked가 true 면 stata의 isLiked값이 false로 바뀌고, false면 true로 바뀐다. 

     

    >Movie.js 파일

    const LIKE_MOVIE = gql`
      mutation toggleLikeMovie($id: Int!, $isLiked: Boolean!) {
        toggleLikeMovie(id: $id, isLiked: $isLiked) @client
      }
    `;

    gql 부분이 위와 같이 바뀌었다. 

     

    export default ({ id, bg, isLiked }) => {
      const [toggleMovie] = useMutation(LIKE_MOVIE, {
        variables: { id: parseInt(id), isLiked }
      });
    
      return (
        <Container>
          <Link to={`/${id}`}>
            <Poster bg={bg} />
          </Link>
          <button onClick={toggleMovie}>{isLiked ? "Unlike" : "Like"}</button>
        </Container>
      );
    };
    

    mutation의 isLiked 함수는 id와 isLiked(boolean) 2개를 받기 때문에 변수에 isLiked 추가한다.

     

    그리고, onClick 시 toggleMovie 함수가 실행되도록 설정한다. 

     

    이제 home 화면에서의 state값인 isLiked 부분의 설정은 완료 되었다. 그렇다면 해당 state값을 detail 페이지에

    연동시키려면 어떻게 해야 할까?

     

    >Detail.js 파일

    const GET_MOVIE = gql`
      query getMovie($id: Int!) {
        movie(id: $id) {
          id
          title
          medium_cover_image
          language
          rating
          description_intro
          isLiked @client
        }
      }
    `;

    id 와 isLiked를 추가해준다. isLiked를 추가해준 이유는, 해당 state값을 해당 페이지에 활용하기 위함이고, id를 추가해준 이유는,apollo가 똑똑하지 않아서 home에서 state를 변경하더라도 그 값이 다른 페이지에 연동이 되지 않기 때문에 id의 값으로 연결을 해주는 것이다.

     

     

            <Title>
              {loading
                ? "Loading..."
                : `${data.movie.title} ${
                    data.movie.isLiked ? "싫어요!" : "좋아요!"
                  }`}
            </Title>

    위와 같이 return부분에 isLiked의 값을 사용할 수 있다.

    'graphql > graphql+nodejs(노마드코더)' 카테고리의 다른 글

    기초적인 graphql과 nodesjs 다루기  (0) 2020.02.28
Designed by Tistory.