• 넘블 Next.js로 추억의 싸이월드 구현하기 챌린지 회고

    2022. 11. 3.

    by. Dev...

    최근에 인스타그램의 광고를 보고 챌린지를 살펴봤는데 재밌어 보이기도 하고 Next.js, GraphQL도 공부하면 좋을 것 같아 신청하게 되었다.

    상세 일정

    10월 14일(금) ~ 11월 4일(목) (21일)

    11월 3일 (목) 자정까지 최종 결과물 제출

     

    스택

    • Next.js
    • Typescript
    • GraphQL
    • Apollo
    • SCSS

    Next.js는 필수이고, GraphQL, Apollo는 아예 처음 써봐서 급하게 공부했다.. 타입스크립트와 SCSS는 최근에 공부를 시작해서 사용했다.

     


    GraphQL 이란

    더보기

    Graphql은 라이브러리나 데이터베이스가 아니라 API를 위한 쿼리 언어. 타입 시스템을 사용하여 쿼리를 실행하는 서버사이드 런타임.

    • 필요한 정보들만 선택하여 받아올 수 있다. Overfetching 문제를 해결. 데이터 전송량 감소.
    • 여러 계층의 정보들을 한 번에 받아올 수 있다. Underfetching 문제를 해결. 요청 횟수 감소.
    • 하나의 endpoint에서 모든 요청을 처리. 하나의 URI에 POST로 요청을 처리.

    프론트와 백을 연결하는 것은 어떤 종류의 데이터를 허용하는지에 대한 특정 표준을 설정하는 graphql api이다.

     

    graphql api의 스키마는 데이터베이스의 스키마와는 다르다. graphql은 데이터베이스와는 완전히 독립적이기 때문이다.

     

    스키마는 어떤 종류의 데이터를 받고 보낼지, 변경할지를 포함해서 API의 작동방식을 설명하는 것이다.


    모든 스키마는 query, mutation 두 가지 필수 타입을 가진다. query는 data를 fetch, read 하는 데 사용되고, mutation은 데이터를 create, update, delete 하는 데 사용된다.


    모든 query와 mutation을 rest api의 endpoint와 같이 사용한다.

    스칼라 타입 - GraphQL 내장 자료형

    • ID: 기본적으로는 String, 고유 식별자 역할임을 나타냄
    • String: UTF-8 문자열
    • Int: 부호가 있는 32비트 정수
    • Float: 부호가 있는 부동소수점 값
    • Boolean: 참/거짓

    Enum 타입 - 특정 값들로 제한되는 특별한 종류의 스칼라

    참고 - https://youtu.be/Zg4XIpnLWQg, https://youtu.be/9BIXcXHsj0A


    NextJS

    # getServerSideProps

    더보기

    page에서 getServerSideProps라는 이름의 함수를 export 할 경우 Next.js는 각 요청마다 해당 page를 getServerSideProps에서 반환되는 데이터를 이용해 pre-render 한다.

    export async function getServerSideProps(context) {
      return {
        props: {}, // 해당 page의 props로 넘어감
      }
    }


    getServerSideProps는 오직 서버 사이드에서만 실행되고 브라우저에서는 실행이 되지 않는다.

    해당 page를 직접적으로 요청할 경우 getServerSideProps는 요청 시점에 실행되고 그 page는 반환된 props와 함께 pre-render 된다.

    해당 page를 클라이언트 사이드에서 페이지 전환(next/router or next/link)으로 요청했다면 getServerSideProps를 실행하고 서버에 API 요청을 보낸다.

    getServerSideProps는 요청 시점에 데이터를 불러와야 하는 page를 렌더 해야 할 때 사용한다. 그게 아니라면 클라이언트 사이드에서 혹은 getStaticProps에서 데이터를 불러와야 한다.

    # getStaticProps

    더보기

    page에서 이 함수를 export 할 경우 Next.js는 getStaticProps에서 반환되는 props를 이용해 빌드 시점에 한 번만 pre-render 한다. getStaticProps도 서버 사이드에서만 실행된다.

     

    ## getStaticProps를 사용하는 경우

    • 해당 page를 render하기위해 필요한 데이터가 사용자의 요청보다 먼저 유효해야할 때
    • 데이터가 headless CMS로부터 올 때
    • 해당 page가 반드시 빠르게 pre-render되어야 할 경우 (for SEO). getStaticProps는 Html과 Json을 생성하는데, 모두 CDN에 의해 cache될 수 있다.
    • 어떤 사용자에 특정된 것이 아닌 공개적인 데이터가 cache되어야 할 경우.

    ## getStaticProps가 실행되는 시점

    • `getStaticProps` always runs during `next build`
    • `getStaticProps` runs in the background when using `[fallback: true]
    • `getStaticProps` is called before initial render when using `[fallback: blocking]
    • `getStaticProps` runs in the background when using `revalidate`
    • `getStaticProps` runs on-demand in the background when using `[revalidate()]

    Apollo Client

    더보기

    Apollo client는 GraphQL API를 호출하기 위해 사용되는 라이브러리
    > GraphQL 쿼리를 작성하기만 하면 Apollo Client가 데이터를 요청하고 캐싱하고 UI까지 업데이트

    Trouble Shooting

    문제: Error: getStaticPaths is required for dynamic SSG pages and is missing for '/diary/[number]'.

     

    동적 라우팅을 하는 다이어리의 상세페이지인 [number].tsx를 만들었다.

     

    이 에러는 동적 라우팅을 하는 페이지에서 getStaticProps를 쓰려면 getStaticPaths를 같이 써야 한다는 것이다. 처음에는 getStaticProps만 사용했지만 getStaticPaths도 추가로 작성해서 해결했다.

     

    getStaticPaths는 페이지가 동적 라우팅 + getStaticProps를 사용하는 경우에 정적으로 렌더링 할 경로를 설정하기 위한 함수이다.

    // pages/diary/[number].tsx
    
    function DiaryDetailPage({ diary }: DiaryData) {
      return <DiaryDetail diary={diary} />;
    }
    
    export async function getStaticPaths() {
      const { data } = await client.query<{ diariesCount: number }>({
        query: GET_DIARIES_COUNT,
      });
    
      const pages = Math.ceil(data.diariesCount / 10);
    
      const paths: { params: { number: string } }[] = [];
      for (let i = 1; i <= pages; i++) {
        const { data } = await client.query<{ diaries: { number: number }[] }>({
          query: GET_DIARIES_NUMBER,
          variables: { page: i },
        });
    
        paths.push(
          ...data.diaries.map((diary) => ({
            params: { number: String(diary.number) },
          })),
        );
      }
    
      return { paths, fallback: true };
    }
    
    export async function getStaticProps({ params }: GetStaticPropsContext) {
      const number = params?.number || '';
    
      const { data, error } = await client.query<DiaryData>({
        query: GET_DIARY,
        variables: { number: Number(number) },
      });
    
      if (!data?.diary || error) return { props: { diary: null } };
      return { props: { diary: data.diary } };
    }
    
    export default DiaryDetailPage;

    getStaticPaths에서 모든 다이어리들의 번호를 받아서 정적 생성할 경로들을 정의하고, getStaticProps에서는 동적 라우팅으로 받은 number에 해당하는 다이어리를 불러와서 해당 페이지의 props로 넘겨준다.

     

    하지만 모든 사람이 모든 다이어리를 수정할 수 있으므로 글 내용이 쉽게 바뀔 것 같아 실제로는 이 코드를 사용하지 않고 client side에서 데이터를 불러왔다.

     

     

    문제:  useMutation으로 글 생성 or 삭제를 하고 글 목록으로 이동하면 반영이 안 되어 있다.

     

    계속 cache에 있는 데이터만 가져와서 보여줘서 그런 것 같다.

    글 생성, 삭제를 한 후, cache를 업데이트하게 하고 글 목록을 보여주는 페이지에서는 페이지로 들어올 때마다 refetch를 하게 했다. fetchPolicy를 'cache-and-network'로 설정해서도 해결 가능.

      const [writeDiary] = useMutation<WriteDiaryResponse, WriteDiaryVars>(WRITE_DIARY, {
        update: (cache, { data }) => {
          // 게시물을 생성하면 캐시에서 게시물을 추가
          const queryData = cache.readQuery<DiariesData>({ query: GET_DIARIES });
          const localDiaries = queryData?.diaries || [];
    
          const newNumber = data?.writeDiaryResponse?.number;
          const newDiary = { writer, title, createdAt: new Date().toISOString(), number: newNumber };
    
          if (localDiaries.some(({ number }) => number === newNumber)) return;
          cache.writeQuery({ query: GET_DIARIES, data: { diaries: [newDiary, ...localDiaries] } });
        },
      });

     

      const [deleteDiary] = useMutation<DeleteDiaryResponse, DeleteDiaryVars>(DELETE_DIARY, {
        variables: { number: Number(number) },
        update: (cache, { data }) => {
          // 삭제를 완료하면 캐시에서 게시물을 삭제
          const queryData = cache.readQuery<DiariesData>({ query: GET_DIARIES });
          const localDiaries = queryData?.diaries;
    
          const deletedNumber = data?.deleteDiaryResponse?.number;
          const newDiaries = localDiaries?.filter((diary) => diary.number !== deletedNumber);
    
          cache.writeQuery({ query: GET_DIARIES, data: { diaries: newDiaries } });
        },
      });
    // 글 목록
    function DiarySection() {
      const { data, loading, error, refetch } = useQuery<DiariesData>(GET_DIARIES);
    
      useEffect(() => {
        // 다이어리 페이지로 들어올 때 마다 refetch
        refetch();
      }, [refetch]);
      ...
      
    // 또는
    function DiarySection() {
      const { data, loading, error } = useQuery<DiariesData>(GET_DIARIES, {
          fetchPolicy: 'cache-and-network',
        });
      ...

     

    수정하기로 페이지를 데이터 넘겨주기

    글 수정 페이지로 이동했을 때 기존 내용을 그대로 표시하려고 했다.

    next/Link 컴포넌트에 query로 글의 정보들을 넘겨주기로 했다. 이렇게 하면 url에 쿼리 스트링이 붙어서 사용자들이 볼 수 있는데 as로 보이지 않게 했다.

    댓글