Spring Security + JWT + React - 06. 프론트엔드 - 게시판 제작: 구조 / 상태관리

june·2022년 8월 23일
0
post-thumbnail

프론트 엔드 구조

전체적인 사용 스펙은 전의 사용했던 스펙과 동일하다.

구현기능

  • 게시물 페이징 리스트
    - 게시물 작성
  • 게시물 조회
    - 게시물 수정
    - 게시물 삭제
  • 추천: 조회, 생성, 삭제
  • 댓글: 조회, 생성, 삭제

게시물 페이징 리스트

페이징으로 불러온 리스트를 보여준다.

맨 아래 있는 페이징 컴포넌트를 통해, 게시판 페이지 이동이 가능하다.

리스트의 제목을 클릭하면 게시물 개별 화면으로 이동하게 된다.

맨 아래 게시물 작성 기능 또한 추가해 놓는다.

게시물 조회

단일 게시물 한개를 보여준다.

수정과 삭제도 가능하다.

화면적 구성으로는 게시물 아래, 추천과 댓글이 위치해있다.

추천

게시물과 댓글 사이에 화면상 위치해있으며, 추천 수 조회와, 추천, 추천 취소(삭제)가 가능하다.

댓글

추천 아래에 위치해있으며, 댓글 조회와, 댓글 작성, 댓글 삭제가 가능하다.

작동 구조 및 화면

react-router-dom으로 구성한 pages 컴포넌트를 중심으로 작동 구조를 파악해보자.

만들어낼 pages 컴포넌트는 총 4개이며, 화면 또한 총 4개로 구성된다.

먼저 페이징 된 게시판을 보여주는 ArticleListPage.tsx와 개별 게시물 하나를 보여주는 ArticleOnePage.tsx이다.

ArticleListPage.tsxArticleList.tsx하나로 구성되어 있으며, ArticleList.tsx는 페이징 이동 컴포넌트인 Paging.tsx를 가지고 있다.

ArticleOnePage.tsx는 게시물 자체만을 보여주는 ArticleOne.tsxRecommend.tsx, Comment.tsx로 구성되어 있다. 이 중 ArticleOne.tsx는 삭제, 수정 등의 버튼을 제외한 게시물만의 정보를 보여주는 Article.tsx를 소유하고 있다.

ArticleList.tsx의 게시물 가운데 하나의 제목을 누르게 되면 자동으로 ArticleOnePage.tsx로 이동하게 되어 페이지를 보여주게 된다.

두개의 page컴포넌트 모두, useParams를 통해 id를 포함하고 있는 컴포넌트에 주입하며, 각각 컴포넌트는 이를 기반으로 컴포넌트 내에 있는 함수들을 실행하여, CRUD를 수행한다.

나머지 2개의 컴포넌트는 CreateArticlePage.tsxUpdateArticlePage.tsx이며, CreateArticleForm.tsx이라는 하나의 컴포넌트로 이 2개의 page컴포넌트를 수행해낸다.

CreateArticlePage.tsx는 말 그래도, 게시물을 생성하는 것이기 때문에, 서버로부터 받아와야할 데이터가 없지만, UpdateArticlePage.tsx는 이전의 게시물을 수정하는 것이기 때문에, useParams를 통한 articleId를 통해 컴포넌트 내에 있는 함수를 실행하여, 이전의 게시물 데이터를 불러온다.

상태관리

action 로직

이전 글에서도 말했듯이, 나는 로그인 로직을 짤 때, 구조가 많이 반복되어 있었기 때문에 Fetch에 관한 부분은 따로 ts파일을 만들어 추상화 했으며

side effect를 불러일으킬 수 있는 action들을 따로 분리하여 ts파일로 만들어, 이후 Context API에서 action을 함수로 호출하여 전역상태와 연결하는 방식으로 로직을 구현했다.

게시판 로직도 마찬가지의 방식으로 구현했다.

Article

/store/article-action.ts

import { GET, POST, PUT, DELETE }  from "./fetch-action";

interface PostArticle {
  id? : string,
  title: string,
  body: string
}

const createTokenHeader = (token:string) => {
  return {
    headers: {
      'Authorization': 'Bearer ' + token
    }
  }
}

export const getPageList = (param:string) => {
  const URL = '/article/page?page=' + param;
  const response = GET(URL, {});
  return response;
}

export const getOneArticle = (param:string, token?:string) => {
  const URL= '/article/one?id=' + param;
  if (!token) {
    const response = GET(URL, {});
    return response;
  } else {
    const response = GET(URL, createTokenHeader(token));
    return response;
  }
};

export const makeArticle = (token:string, article:PostArticle) => {
  const URL = '/article/';
  const response = POST(URL, article, createTokenHeader(token));
  return response;
};

export const getChangeArticle = (token:string, param:string) => {
  const URL = '/article/change?id=' + param;
  const response = GET(URL, createTokenHeader(token));
  return response;
};

export const changeArticle = (token:string, article:PostArticle) => {
  const URL = '/article/';
  const response = PUT(URL, article, createTokenHeader(token));
  return response;
};

export const deleteArticle = (token:string, param:string) => {
  const URL = '/article/one?id=' + param;
  const response = DELETE(URL, createTokenHeader(token));
  return response;
}

하나하나 설명을 하자면, 먼저 PostArticle은 주어진 데이터가 워낙 많이 쓰이기 때문에 하나의 interface로 만들어서, 변수가 인터페이스를 타입으로 쓰는 방식으로 로직을 단순화 했다.

createTokenHeader는 이전에서도 사용했듯이, 토큰을 만들기 위한 함수다.

getPageList는 원하는 페이지가 string으로 주어지면, 그것을 바탕으로 URL을 만들어서 서버에 요청하여 데이터를 받는 함수다.

getOneArticle또한 마찬가지의 로직이지만, 수정/삭제 와 같은 버튼이 표기되어야 하는가 아닌가의 여부가 정해져야 하므로, 토큰을 넣을 수 있게 함수를 만들었다.

makeArticle 은 게시물을 만드는 함수다. article이라는 매개변수가 PostArticle이라는 인터페이스를 타입으로 사용한다. 토큰값이 필요하다.

getChangeArticle은 수정하기 위해, 이전에 쓰여졌던 데이터를 불러오는 함수다. 게시물의 id와 토큰값이 필요하다.

changeArticle 은 수정된 게시물을 서버에 등록하는 함수다. 게시물의 id와 토큰값이 필요하다.

deleteArticle은 게시물을 삭제하기 위한 함수다. 게시물의 id와 토큰값이 필요하다.

Recommend

/store/recommend-action.ts

import { GET, POST, DELETE }  from "./fetch-action";

const createTokenHeader = (token:string) => {
  return {
    headers: {
      'Authorization': 'Bearer ' + token
    }
  }
}

export const getRecommends = (param:string, token?:string) => {
  const URL = '/recommend/list?id=' + param;
  const response = (token ? GET(URL, createTokenHeader(token)) : GET(URL, {}));
  return response;
}

export const makeRecommend = async (id_str:string, token:string) => {
  const URL = '/recommend/'
  const id = +id_str
  const response = POST(URL, { id:id }, createTokenHeader(token));
  return response;
}

export const deleteRecommend = (param:string, token:string) => {
  const URL = '/recommend/one?id=' + param;
  const response = DELETE(URL, createTokenHeader(token));
  return response;
}

추천 위와 비슷한 로직으로 되어있다.

getRecommends는 추천의 숫자와 추천여부를 조회하는 함수이며, token 여부에 따라 다른 메소드를 보내며, article의 id를 매개변수로 가진다.

makeRecommend는 추천을 생성하는 함수로, 마찬가지로 article의 id를 매개변수로 가지며, token이 포함되어있다.

deleteRecommend는 추천을 삭제하는 함수로, 마찬가지로 article의 id를 매개변수로 가지며, token이 포함되어있다.

Comment

/store/comment-action.ts

import { GET, POST, DELETE }  from "./fetch-action";

type Comment = {
  articleId: string,
  body: string
}

const createTokenHeader = (token:string) => {
  return {
    headers: {
      'Authorization': 'Bearer ' + token
    }
  }
}

export const getComments = (param:string, token?:string) => {
  const URL = '/comment/list?id=' + param;
  const response = (token ? GET(URL, createTokenHeader(token)) : GET(URL, {}));
  return response;
}

export const makeComment = (comment:Comment, token:string) => {
  const URL = '/comment/'
  const response = POST(URL, comment,  createTokenHeader(token));
  return response;
}

export const deleteComment = (param:string, token:string) => {
  const URL = '/comment/one?id=' + param;
  const response = DELETE(URL, createTokenHeader(token));
  return response;
}

댓글도 Articleinterface로 타입을 설정한것 처럼, Commenttype객체로 만들어서, 매개변수의 type으로 지정했다.

getComments은 댓글 조회함수이며, token 여부에 따라 다른 메소드를 보내며, article의 id를 매개변수로 가진다.

makeComment는 추천을 생성하는 함수로, 마찬가지로 article의 id를 매개변수로 가지며, token이 포함되어있다.

deleteComment는 추천을 삭제하는 함수로, 마찬가지로 article의 id를 매개변수로 가지며, token이 포함되어있다.

Context

Context에 있어서는 구조가 이전의 auth-context.tsx와 다르다.

왜냐하면, 기본적으로 전역상태에 놓이는 게시물, 댓글, 추천등은 객체 형식으로 이루어져 있기 때문에 재사용성을 위해 따로 type으로 분리해놓기 때문이다.

그러나 type으로 분리해놓으면 createContext를 통해, 전역상태의 기본값을 설정하는 객체에서, state의 value를 type으로 지정할 수 없게 된다.

따라서 새로운 interface를 생성하여, 기본적인 값을 세팅한 다음, createContext의 제네릭 값을 해당 interface로 설정하는 방식으로 구조를 바꾸었다.

Article

/store/article-context.ts

import React, { useState, useEffect, useCallback, useRef } from "react";
import * as articleAction from './article-action';

type Props = { children?: React.ReactNode }
type ArticleInfo = {
  articleId: number,
  memberNickname: string,
  articleTitle: string,
  articleBody: string,
  cratedAt: string,
  updatedAt?: string,
  isWritten?: boolean
};

interface PostArticle {
  id? : string,
  title: string,
  body: string
} 

interface Ctx {
  article?: ArticleInfo | undefined;
  page: ArticleInfo[];
  isSuccess: boolean;
  isGetUpdateSuccess: boolean;
  totalPages: number;
  getPageList: (pageId: string) => void;
  getArticle: (param:string, token?:string) => void;
  createArticle: (article:PostArticle, token:string) => void;
  getUpdateArticle: (token:string, param:string) => void;
  updateArticle: (token:string, article:PostArticle) => void;
  deleteArticle: (token:string, param:string) => void;
}



const ArticleContext = React.createContext<Ctx>({
  article: undefined,
  page: [],
  isSuccess: false,
  isGetUpdateSuccess: false,
  totalPages: 0,
  getPageList: () => {},
  getArticle: ()=>{},
  createArticle:  ()=>{},
  getUpdateArticle: ()=>{},
  updateArticle: ()=>{},
  deleteArticle: ()=>{}
});

export const ArticleContextProvider:React.FC<Props> = (props) => {

  const [article, setArticle] = useState<ArticleInfo>();
  const [page, setPage] = useState<ArticleInfo[]>([]);
  const [totalPages, setTotalPages] = useState<number>(0);
  const [isSuccess, setIsSuccess] = useState<boolean>(false);
  const [isGetUpdateSuccess, setIsGetUpdateSuccess] = useState<boolean>(false);


  const getPageHandlerV2 = async (pageId: string) => {
    setIsSuccess(false);
    const data = await articleAction.getPageList(pageId);
    const page:ArticleInfo[] = data?.data.content; 
    const pages:number = data?.data.totalPages;
    setPage(page);
    setTotalPages(pages);
    setIsSuccess(true);
  }

  const getArticleHandler = (param:string, token?:string) => {
    setIsSuccess(false);
    const data = (token ? 
      articleAction.getOneArticle(param, token)
      : articleAction.getOneArticle(param))
    data.then((result) => {
      if (result !== null) {
        const article:ArticleInfo = result.data;
        setArticle(article);
        
        
      }
    })
    setIsSuccess(true);
  }

  const createArticleHandler = (article:PostArticle, token:string) => {
    setIsSuccess(false);
    const data = articleAction.makeArticle(token, article);
    data.then((result) => {
      if (result !== null) {
        console.log(isSuccess);
        
      }
    })
    setIsSuccess(true);
  }

  const getUpdateArticleHancler = async (token:string, param:string) => {
    setIsGetUpdateSuccess(false);
    const updateData = await articleAction.getChangeArticle(token, param);
    const article:ArticleInfo = updateData?.data;
    setArticle(article);
    setIsGetUpdateSuccess(true);
  }


  const updateArticleHandler = (token:string, article:PostArticle) => {
    setIsSuccess(false);
    console.log('update api start')
    const data = articleAction.changeArticle(token, article);
    data.then((result) => {
      if (result !== null) {
        
      }
    })
    setIsSuccess(true);
  }

  const deleteArticleHandler = (token:string, param:string) => {
    setIsSuccess(false);
    const data = articleAction.deleteArticle(token, param);
    data.then((result) => {
      if (result !== null) {
        
      }
    })
    setIsSuccess(true);

  }

  const contextValue:Ctx = {
    article,
    page,
    isSuccess,
    isGetUpdateSuccess,
    totalPages,
    getPageList: getPageHandlerV2,
    getArticle: getArticleHandler,
    createArticle: createArticleHandler,
    getUpdateArticle: getUpdateArticleHancler,
    updateArticle: updateArticleHandler,
    deleteArticle: deleteArticleHandler
  }

  return (
  <ArticleContext.Provider value={contextValue}>
    {props.children}
  </ArticleContext.Provider>)
}

export default ArticleContext;

하나하나 살펴보도록 하자.

먼저 Props에 관한 부분은 지난 글에 다뤘기 때문에 생략한다.

ArticleInfo, PostArticle

type ArticleInfo = {
  articleId: number,
  memberNickname: string,
  articleTitle: string,
  articleBody: string,
  cratedAt: string,
  updatedAt?: string,
  isWritten?: boolean
};

interface PostArticle {
  id? : string,
  title: string,
  body: string
}

ArticleInfoContext뿐만 아니라 다른 컴포넌트에서도 많이 쓰이게 되는 type이며, PostArticle은 컴포넌트로부터, 게시물의 수정이나 삭제를 위해 받는 인터페이스다.

Ctx, ArticleContext

interface Ctx {
  article?: ArticleInfo | undefined;
  page: ArticleInfo[];
  isSuccess: boolean;
  isGetUpdateSuccess: boolean;
  totalPages: number;
  getPageList: (pageId: string) => void;
  getArticle: (param:string, token?:string) => void;
  createArticle: (article:PostArticle, token:string) => void;
  getUpdateArticle: (token:string, param:string) => void;
  updateArticle: (token:string, article:PostArticle) => void;
  deleteArticle: (token:string, param:string) => void;
}



const ArticleContext = React.createContext<Ctx>({
  article: undefined,
  page: [],
  isSuccess: false,
  isGetUpdateSuccess: false,
  totalPages: 0,
  getPageList: () => {},
  getArticle: ()=>{},
  createArticle:  ()=>{},
  getUpdateArticle: ()=>{},
  updateArticle: ()=>{},
  deleteArticle: ()=>{}
});

Ctx는 앞서 말했듯이, 객체를 전역상태로 돌리기 위한 인터페이스며, 그것을 ArticleContextcreateContext의 제네릭값으로 삼았다.

그리고 Ctx안에서 article의 type을 ArticleInfo | undefined으로 설정해놨기 때문에 객체 안의 article의 value를 undefined로 삼을 수 있게 되었다.

상세한 값들은 Provider에서 설명하도록 한다.

ArticleContextProvider

export const ArticleContextProvider:React.FC<Props> = (props) => {

  const [article, setArticle] = useState<ArticleInfo>();
  const [page, setPage] = useState<ArticleInfo[]>([]);
  const [totalPages, setTotalPages] = useState<number>(0);
  const [isSuccess, setIsSuccess] = useState<boolean>(false);
  const [isGetUpdateSuccess, setIsGetUpdateSuccess] = useState<boolean>(false);

...

  const contextValue:Ctx = {
    article,
    page,
    isSuccess,
    isGetUpdateSuccess,
    totalPages,
    getPageList: getPageHandlerV2,
    getArticle: getArticleHandler,
    createArticle: createArticleHandler,
    getUpdateArticle: getUpdateArticleHancler,
    updateArticle: updateArticleHandler,
    deleteArticle: deleteArticleHandler
  }

  return (
  <ArticleContext.Provider value={contextValue}>
    {props.children}
  </ArticleContext.Provider>)
}

Context의 변화를 알리는 Provider컴포넌트를 반환하는 함수다.

전역상태를 보자면 article은 앞서 설정한 ArticleInfo의 형태로 이루어질 전역상태이며,

pageArticleInfo로 이루어진 리스트 형태로 이루어진 전역상태다.

전체 페이지를 알려주는 totalPagesnumber로 되어있고,

각각 fetch함수의 성공 여부를 통해 컴포넌트가 실행해야할 값을 알려주는 isSuccessisGetUpdateSuccessboolean값으로 되어있다.

getPageHandler

  const getPageHandler = async (pageId: string) => {
    setIsSuccess(false);
    const data = await articleAction.getPageList(pageId);
    const page:ArticleInfo[] = data?.data.content; 
    const pages:number = data?.data.totalPages;
    setPage(page);
    setTotalPages(pages);
    setIsSuccess(true);
  }

게시물 리스트를 얻는 함수다. 매개변수는 원하는 페이지의 페이지 값이다.

앞서 백엔드에서 구축한 값에서 Page객체로 이루어진 값을 보내기 때문에, 리스트는 물론 전체 페이지수에 대한 값도 추출해낼 수 있다.

getArticleHandler

  const getArticleHandler = (param:string, token?:string) => {
    setIsSuccess(false);
    const data = (token ? 
      articleAction.getOneArticle(param, token)
      : articleAction.getOneArticle(param))
    data.then((result) => {
      if (result !== null) {
        const article:ArticleInfo = result.data;
        setArticle(article);
      }
    })
    setIsSuccess(true);
  }

게시물 한 개를 얻는 함수다. token여부에 따라 action 함수에 넣는 매개변수의 값이 달라진다.

createArticleHandler

  const createArticleHandler = (article:PostArticle, token:string) => {
    setIsSuccess(false);
    const data = articleAction.makeArticle(token, article);
    data.then((result) => {
      if (result !== null) {
        console.log(isSuccess);
      }
    })
    setIsSuccess(true);
  }

게시물 생성함수다. 토큰값과 PostArticle타입의 객체를 매개변수로 받는다.

getUpdateArticleHancler

  const getUpdateArticleHancler = async (token:string, param:string) => {
    setIsGetUpdateSuccess(false);
    const updateData = await articleAction.getChangeArticle(token, param);
    const article:ArticleInfo = updateData?.data;
    setArticle(article);
    setIsGetUpdateSuccess(true);
  }

수정을 할 때 이 전에 적었던 게시물 내용을 불러오는 함수다.

수정할 게시물의 번호와 토큰을 매개변수로 받는다.

수정은 생성과 동일한 컴포넌트를 사용하여 만들 것이기 때문에, 성공 여부를 알려주는 전역상태는 isSuccess가 아니라isGetUpdateSuccess를 사용한다.

updateArticleHandler

  const updateArticleHandler = (token:string, article:PostArticle) => {
    setIsSuccess(false);
    const data = articleAction.changeArticle(token, article);
    data.then((result) => {
      if (result !== null) {
        
      }
    })
    setIsSuccess(true);
  }

수정한 게시물을 서버에 등록하는 함수다. 생성과 마찬가지로 PostArticle타입의 객체를 매개변수로 삼는다.

deleteArticleHandler

  const deleteArticleHandler = (token:string, param:string) => {
    setIsSuccess(false);
    const data = articleAction.deleteArticle(token, param);
    data.then((result) => {
      if (result !== null) {
        
      }
    })
    setIsSuccess(true);
  }

게시물을 삭제하는 함수다. 토큰값과 삭제할 게시물의 id를 매개변수로 갖는다.

Recommend

추천도 게시판 논리와 매우 유사하다.

/store/recommend-context.ts

import React, { useState } from "react";

import * as recommendAction from './recommend-action';

type Props = { children?: React.ReactNode }

type Recommends = {
  recommendNum: number
  recommended: boolean
}


interface RecommendCtx {
  recommends: Recommends | undefined;
  isSuccess: boolean;
  isChangeSuccess: boolean;
  getRecommends: (param:string, token?:string) => void;
  postRecommend: (id:string, token:string) => void;
  deleteRecommend: (param:string, token:string) => void;
}

const RecommendContext = React.createContext<RecommendCtx>({
  recommends: undefined,
  isSuccess: false,
  isChangeSuccess: false,
  getRecommends: () => {},
  postRecommend: () => {},
  deleteRecommend: () => {},
});

export const RecommendContextProvider:React.FC<Props> = (props) => {

  const [recommends, setRecommends] = useState<Recommends>();
  const [isSuccess, setIsSuccess] = useState<boolean>(false);
  const [isChangeSuccess, setIsChangeSuccess] = useState<boolean>(false);

  const getRecommendsHandler = (param:string, token?:string) => {
    setIsSuccess(false);
    const data = (token ? 
      recommendAction.getRecommends(param, token) 
      : recommendAction.getRecommends(param));
    data.then((result) => {
      if (result !== null) {
        const recommends:Recommends = result.data;
        setRecommends(recommends);
      }
    })
    setIsSuccess(true);
  }

  const postRecommendHandler = async (id:string, token:string) => {
    setIsChangeSuccess(false);
    const postData = await recommendAction.makeRecommend(id, token);
    const msg = await postData?.data;
    console.log(msg);

    const getData = await recommendAction.getRecommends(id, token);
    const recommends:Recommends = getData?.data;
    setRecommends(recommends);
    setIsChangeSuccess(true);
  }

  const deleteRecommendHancler = async (param:string, token:string) => {
    setIsChangeSuccess(false);
    const deleteData = await recommendAction.deleteRecommend(param, token);
    const msg = await deleteData?.data;

    const getData = await recommendAction.getRecommends(param, token);
    const recommends:Recommends = getData?.data;
    setRecommends(recommends);
    setIsChangeSuccess(true);
  }


  const contextValue:RecommendCtx = {
    recommends,
    isSuccess,
    isChangeSuccess,
    getRecommends: getRecommendsHandler,
    postRecommend: postRecommendHandler,
    deleteRecommend: deleteRecommendHancler
  }

  return(
    <RecommendContext.Provider value={contextValue}>
      {props.children}
    </RecommendContext.Provider>
  )
}

export default RecommendContext;

여기서는 추천 수와, 추천 여부로 이루어진 Recommends 타입을 사용한다. 이외의 기본적인 typeinterface 그리고 createContext에 관련된 내용은 중복되므로 생략하겠다.

State

  const [recommends, setRecommends] = useState<Recommends>();
  const [isSuccess, setIsSuccess] = useState<boolean>(false);
  const [isChangeSuccess, setIsChangeSuccess] = useState<boolean>(false);

3가지의 state를 사용하며, 하나는 앞서 말한 추천 객체인 Recommends, 또 하나는 Article에서도 사용하는, 성공 여부를 알려주는 isSuccess, 나머지 하나는 추천 생성, 삭제가 제대로 이루어져있는지 알려주는 isChangeSuccess다.

getRecommendsHandler

  const getRecommendsHandler = (param:string, token?:string) => {
    setIsSuccess(false);
    const data = (token ? 
      recommendAction.getRecommends(param, token) 
      : recommendAction.getRecommends(param));
    data.then((result) => {
      if (result !== null) {
        const recommends:Recommends = result.data;
        setRecommends(recommends);
      }
    })
    setIsSuccess(true);
  }

추천을 불러오는 함수다. 매개변수는 토큰의 여부에 따라 갈릴 수 있다.

postRecommendHandler

  const postRecommendHandler = async (id:string, token:string) => {
    setIsChangeSuccess(false);
    const postData = await recommendAction.makeRecommend(id, token);
    const msg = await postData?.data;

    const getData = await recommendAction.getRecommends(id, token);
    const recommends:Recommends = getData?.data;
    setRecommends(recommends);
    setIsChangeSuccess(true);
  }

추천을 생성하는 함수다. 게시물의 id와 토큰을 매개변수로 받는다.

deleteRecommendHancler

  const deleteRecommendHancler = async (param:string, token:string) => {
    setIsChangeSuccess(false);
    const deleteData = await recommendAction.deleteRecommend(param, token);
    const msg = await deleteData?.data;

    const getData = await recommendAction.getRecommends(param, token);
    const recommends:Recommends = getData?.data;
    setRecommends(recommends);
    console.log(recommends);
    setIsChangeSuccess(true);
  }

추천을 삭제하는 함수다. 마찬가지로 게시물의 id와 토큰을 매개변수로 받는다.

Comment

댓글도 마찬가지로 유사하므로 간단하게 짚고만 넘어가겠다.

import React, { useState } from "react";

import * as commentAction from './comment-action';

type Props = { children?: React.ReactNode }
type CommentInfo = {
  commentId: number,
  memberNickname: string,
  commentBody: string,
  createdAt: Date,
  written: boolean
}

type PostComment = {
  articleId: string,
  body: string
}



interface CommentCtx {
  commentList: CommentInfo[];
  isSuccess: boolean;
  getComments: (param:string, token?:string) => void;
  createComment: (comment:PostComment, token:string) => void;
  deleteComment: (param:string, id:string, token:string) => void;
}

const CommentContext = React.createContext<CommentCtx>({
  commentList: [],
  isSuccess: false,
  getComments: () => {},
  createComment: () => {},
  deleteComment: () => {},
});

export const CommentContextProvider:React.FC<Props> = (props) => {

  const [commentList, setCommentList] = useState<CommentInfo[]>([]);
  const [isSuccess, setIsSuccess] = useState<boolean>(false);

  const getCommentsHandler = async (param:string, token?:string) => {
    setIsSuccess(false);
    const data = (
      token ? await commentAction.getComments(param, token) : await commentAction.getComments(param)
    );
    const comments:CommentInfo[] = await data?.data;
    setCommentList(comments);
    setIsSuccess(true);
  }

  const createCommentHandler = async (comment:PostComment, token:string) => {
    setIsSuccess(false); 
    const postData = await commentAction.makeComment(comment, token);
    const msg = await postData?.data;

    const getData = await commentAction.getComments(comment.articleId, token);
    const comments:CommentInfo[] = getData?.data;
    setCommentList(comments);
    setIsSuccess(true);
  };

  const deleteCommentHandler = async(param:string,id:string, token:string) => {
    setIsSuccess(false);
    const deleteData = await commentAction.deleteComment(param, token);
    const msg = deleteData?.data;

    const getData = await commentAction.getComments(id, token);
    const comments:CommentInfo[] = getData?.data;
    setCommentList(comments);
    setIsSuccess(true);
  };


  const contextValue:CommentCtx = {
    commentList,
    isSuccess,
    getComments: getCommentsHandler,
    createComment: createCommentHandler,
    deleteComment: deleteCommentHandler
  }

  return (
    <CommentContext.Provider value={contextValue}>
      {props.children}
    </CommentContext.Provider>
  )
}

export default CommentContext;

Type

type CommentInfo = {
  commentId: number,
  memberNickname: string,
  commentBody: string,
  createdAt: Date,
  written: boolean
}

type PostComment = {
  articleId: string,
  body: string
}

댓글의 정보를 가져오는 CommentInfo타입과, 댓글을 작성하기 위해 필요한 PostComment타입으로 구성한다.

State

  const [commentList, setCommentList] = useState<CommentInfo[]>([]);
  const [isSuccess, setIsSuccess] = useState<boolean>(false);

댓글들을 CommentInfo타입의 리스트로 만드는 commentList와 댓글 작성, 삭제 여부를 체크하는 isSuccess로 이루어져 있다.

getCommentsHandler

  const getCommentsHandler = async (param:string, token?:string) => {
    setIsSuccess(false);
    const data = (
      token ? await commentAction.getComments(param, token) : await commentAction.getComments(param)
    );
    const comments:CommentInfo[] = await data?.data;
    setCommentList(comments);
    setIsSuccess(true);
  };

댓글을 불러오는 함수다. 댓글 삭제를 위해, 토큰 여부에 따라 매개변수가 달라진다.

createCommentHandler

  const createCommentHandler = async (comment:PostComment, token:string) => {
    setIsSuccess(false); 
    const postData = await commentAction.makeComment(comment, token);
    const msg = await postData?.data;

    const getData = await commentAction.getComments(comment.articleId, token);
    const comments:CommentInfo[] = getData?.data;
    setCommentList(comments);
    setIsSuccess(true);
  };

댓글을 생성하는 함수다. 앞서말한 PostComment타입 객체와, 토큰을 매개변수로 받는다.

댓글을 생성했으면, 자동으로 댓글 리스트의 값이 달라지므로, 바로 이후에 댓글을 불러오는 함수를 실행하여, state를 재정립한다.

  const deleteCommentHandler = async(param:string,id:string, token:string) => {
    setIsSuccess(false);
    const deleteData = await commentAction.deleteComment(param, token);
    const msg = deleteData?.data;

    const getData = await commentAction.getComments(id, token);
    const comments:CommentInfo[] = getData?.data;
    setCommentList(comments);
    setIsSuccess(true);
  };

댓글을 삭제하는 함수다. .

이 또한 생성과 마찬가지로 먼저 삭제하기 위한 댓글의 id를 불러온 다음, 삭제 시킨 다음, 댓글 리스트를 다시 불러와 state에 재정립시킨다.

이로써, 프론트엔드에서 Context에 관한 부분은 모두 끝났다.

이제 이 Context를 다시 컴포넌트에 적용시켜보자.

profile
초보 개발자

0개의 댓글