본문 바로가기
개발이야기/React

[React] 사진첩 게시판

by dev.josh 2023. 1. 4.
반응형

저번에 작업했던 vue2_photo_board와 컴포넌트 구조, 스타일링, 변수, 함수를 최대한 동을하게 구성하여 해서 진행하였다.

 

목차

1. 환경
2. 반응형 화면
3. 기능
4. 프로젝트 구조
5. 컴포넌트 구조
6. 통신과 랜더링
7. 전체소스

1. 환경

  • node v18.12.1
  • react v18 (useContext, react-query)

2. 반응형 화면

 

3. 기능




1. 사진 상세보기 (모달창 활성화)
2. 페이징 처리
3. 상세페이지 닫기( 모달창 비활성화)







4. 프로젝트 구조



src
⌞ api
⌞ board.js : context방식의 api 인스턴스
⌞ boardClient.js : axios로 직접적인 통신
⌞ components
⌞ layout
⌞ImageLoading.jsx : 이미지 1개 로딩 애니메이션
⌞Loading.jsx : 레이아웃 로딩 애니메이션
⌞ Item.jsx : 게시물1개에 대한 컴포넌트
⌞ List.jsx : 게시물의 목록 컴포넌트
⌞ Modal.jsx : 모달창 컴포넌트
⌞ Paginations.jsx : 페이징 컴포넌트
⌞ context
⌞ BoardApiContext.jsx : 게시판에서 공유되는 함수와 데이터
⌞ ModalContext.jsx : 모달창에서 공유되는 함수와 데이터
⌞ styles : 스타일링 모듈











 

 

 

 

5. 컴포넌트 구조

App.js

import './App.css';
import List from './components/List';
import Modal from './components/Modal';
import { ModalProvider } from './context/ModalContext';
import { BoardApiProvier } from './context/BoardApiContext';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';

const queryClient = new QueryClient();

export default function App() {
  return (
    <>
      <BoardApiProvier>
        <QueryClientProvider client={queryClient}>
          <ModalProvider>
            <List></List>
            <Modal></Modal>
          </ModalProvider>
        </QueryClientProvider>
      </BoardApiProvier>
    </>
  );
}

 

List.jsx

    <>
      <div className={styles.title}>
		...
      <div>
        { 
          isLoading &&
          <div className={styles.loading}>
            <Loading></Loading> 
          </div>
        }
        <div className={styles.wrapper}>
          <div className={styles.container}>
            { error && <p>{JSON.stringify(error.message)}</p> }
            {
              data && 
              data.map((item) => {
                return (<Item key={item.id} item={item} ></Item>);
              })
            }
          </div>
        </div>
        { 
          <Paginations
            className={styles.paginations}
            totalCount={1000}
          ></Paginations>
        }
      </div>
    </>

 

modal.jsx

    <>
      {
        modal &&
        <div className={`${styles.modalMask} ${modal ? styles.show : '' }`}>
          <div className={styles.modalContainer}>
            <div>
              <div className={styles.modalHeader}>
                <h4>photo</h4>
                <div 
                  className={styles.closeBtn}
                  onClick={()=>closeModal()}
                >X</div>
              </div>
                { 
                  isLoading &&
                  <div className={styles.loading}>
                    <Loading></Loading> 
                  </div>
                }
                {
                  error && <div>ERROR!</div>
                }
                {
                  data &&
                  <Item key={Number(data.id)} item={data} wSize={800} useModal={true}></Item>
                }
            </div>
          </div>
        </div>
      }
    </>

 


6. 통신과 랜더링

목록 조회 동작은 List컴포넌트에 자식으로 존재하는 Paginations컴포넌트에서 ReactQuery를 사용하여 데이터를 호출하는 방식으로 진행 하였다. Paginations컴포넌트에서 페이징을 동작할때 Context로 데이터와 함수를 공유하여 하위 자식에 있는 컴포넌트의 데이터를 업데이트 해주었다.

api통신을 위한 인스턴스를 생성하고 데이터를 하위 컴포넌트들과 공유한다. (BoardApiContext.jsx)

import { createContext, useContext, useState } from 'react';
import Board from '../api/board';
import BoardClient from '../api/boardClient';

export const BoardApiContext = createContext();
const client = new BoardClient();
const board = new Board(client);

export function BoardApiProvier({ children }) {
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState(false);
  const [data, setData] = useState([]);
  const [limit, setLimit] = useState(10);

  return (
    <BoardApiContext.Provider value={{ board, isLoading, setIsLoading, error, setError, data, setData, limit, setLimit }}>
      {children}
    </BoardApiContext.Provider>
  );
}

export function useBoardApi() {
  return useContext(BoardApiContext);
}


Paginations 컴포넌트에서 useQuery를 사용하여 Context에서 공유된 인스턴스로 데이터를 불러온다. renderPagination함수 에서는 전체 게시물 개수와 현재 페이지를 받아서 페이징 목록을 그려준다. (전체 게시물 개수는 1000개로 고정함)
Paginations.jsx

import React, { useEffect, useState, useContext } from 'react';
import styles from '../styles/Page.module.css';
import { useImmer } from 'use-immer';
import { useQuery } from '@tanstack/react-query';
import { BoardApiContext, useBoardApi } from '../context/BoardApiContext';

export default function Paginations({totalCount}) {
  const { setIsLoading, setError, setData, limit } = useContext(BoardApiContext);
  const [pageList, setPageList] = useImmer([]);
  const [totalPage, setTotalPage] = useState(0);
  const [pageGroupCount, setPageGroupCount] = useState(10);
  const [currentPage, setCurrentPage] = useState(1);
  const [first, setFirst] = useState(0);
  const [last, setLast] = useState(0);
  const [prev, setPrev] = useState(0);
  const [next, setNext] = useState(0);

  const { board } = useBoardApi();
  const {
    isLoading,
    error,
    data,
  } = useQuery(['board', currentPage, limit], () => board.getPhotoList(currentPage ,limit), { keepPreviousData : true });

  useEffect(() => {
    movePage(currentPage);
  },[totalPage, currentPage, last, first])

  useEffect(() => {
    setIsLoading(isLoading);
    setError(error);
    setData(data);
  },[isLoading, error, data]);

  const movePage = (page) => {
    renderPagination(totalCount, page);
  }

  const renderPagination = (totalCount, cPage) => {
    if (totalCount <= pageGroupCount) return;
    setTotalPage(Math.ceil(totalCount / limit));
    let pageGroup = Math.ceil(currentPage / pageGroupCount);
    setCurrentPage(cPage);
    setLast(pageGroup * pageGroupCount);
    if (last > totalPage) last = totalPage;
    setFirst(last - (pageGroupCount - 1) <= 0 ? 1 : last - (pageGroupCount - 1));
    setNext(last + 1);
    setPrev(first - 1);
    setPageList([]);
    for (let index = first; index <= last; index++) {
      setPageList((totalPage) => {
        totalPage.push(index);
      });
    }
  }

  const prePage = () => {
    setFirst(first - pageGroupCount);
    if (first < 0) first = 1;
    renderPagination(totalCount, first - pageGroupCount);
  }
  const nextPage = () => {
    setLast(last + 1);
    renderPagination(totalCount, last+1);
  }

  return (
    <ul>
      {
        prev > 0 &&
        <li onClick={()=>prePage()}>
          <span>〈</span>
        </li>
      }
      { 
        pageList &&
        pageList.map((page, index) => {
          return (
            <li
              key={index}
              className={`${page === currentPage ? styles.active : ''}`}
              onClick={()=>movePage(page)}
            >
              <span>{page}</span>
            </li>
          )
        })
      }
      {
        last < totalPage &&
        <li onClick={()=>nextPage()}>
          <span>〉</span>
        </li>
      }
    </ul>
  );
}


List컴포넌트에서 데이터 로딩과 에러 처리, Item컴포넌트를 반복해서 랜더링한다.
List.jsx

import React, { useState, useContext }  from 'react';
import styles from '../styles/List.module.css';
import Loading from './layout/Loading';
import Item from './Item';
import Paginations from './Paginations';
import { BoardApiContext } from '../context/BoardApiContext';

export default function List() {
  const { isLoading, error, data, limit, setLimit } = useContext(BoardApiContext);
  const [selectList] = useState([10, 20, 30]);
  const handleChangeSelect = (e) => {
    setLimit(Number(e.target.value));
  };

  return (
    <>
      <div className={styles.title}>
        <h3>photos</h3>
        <div>
          <select onChange={handleChangeSelect} defaultValue={limit} style={{color: 'black'}}>
            {
              selectList.map((item, index) => {
                return (<option key={index} value={item}>{item}</option>)
              })
            }
          </select>
        </div>
      </div>
      <div>
        { 
          isLoading &&
          <div className={styles.loading}>
            <Loading></Loading> 
          </div>
        }
        <div className={styles.wrapper}>
          <div className={styles.container}>
            { error && <p>{JSON.stringify(error.message)}</p> }
            {
              data && 
              data.map((item) => {
                return (<Item key={item.id} item={item} ></Item>);
              })
            }
          </div>
        </div>
        { 
          <Paginations
            className={styles.paginations}
            totalCount={1000}
          ></Paginations>
        }
      </div>
    </>
  );
}


vue에서 작업할때는 데이터 호출을 하기 위해 하위 컴포넌트에서 상위 컴포넌트의 이벤트를 호출하는 방식으로 진행을 했었다.
vue에서 작업했던 구조를 따라가다 보니, react버전에서는 최 하위 컴포넌트에서 데이터를 호출하고 상위 컴포넌트로 올려주는 방식이 되었는데... 이건 정답이 아닌거 같다.

7. 전체 소스

https://github.com/Jo-App/react18_photo_board

 

GitHub - Jo-App/react18_photo_board

Contribute to Jo-App/react18_photo_board development by creating an account on GitHub.

github.com



https://github.com/Jo-App/vue2_photo_board

 

GitHub - Jo-App/vue2_photo_board

Contribute to Jo-App/vue2_photo_board development by creating an account on GitHub.

github.com

 

반응형

'개발이야기 > React' 카테고리의 다른 글

[React] 이벤트 핸들링 HTML과 비교  (0) 2022.11.10