취업(Employment)/프로젝트(Project)

게시판 프로젝트 CRUD (F: React, 혼자 진행)

Zibu 2023. 4. 20. 18:06
반응형

 

 

 

프로젝트를 구현하면서 후기를 적어봤다.

API를 SpringBoot로 구현하려고 한다.

 

 

Context 사용 방법 참고

https://react.vlpt.us/using-typescript/04-ts-context.html

https://react.vlpt.us/mashup-todolist/02-manage-state.html

 

 

 

 

 

 

 

 

 📢 CRUD와 React에 대한 이해 필요

 📢 차후에 댓글 사용자에 따라 공계 비공계 설정 추가 예정

 

 

✔️프로젝트를 진행한 이유

  • 이력서 지원한 회사에서 과제를 줌
  • SI 업체에 취업을 하면 대부분 초반에 테이블 짜는거 많이 한다고 함
  • 테이블 구현하는 것이 CRUD의 기본이라고 함
  • 게시물을 다 보여줄 수 없기 때문에 pagination도 구현해야 함
  • Javascript로 구현하면 타입 오류를 파악 불가능해서 Typescript 실습하기 위함
  • React Hooks를 언제 사용하는지 파악하기 위함

 

 

 

✔️사용한 스택 및 기술

Javascript, Typescript, React, React Hooks, React Router, webpack, babel

 

 

 

✔️미션 내용 요약

  • 실제 있는 Wiki 서비스 같은 게시판을 만들어야 함
  • framework는 React 사용할 것
  • 데이터나 디자인은 자유롭게 사용(레이아웃)
  • Wiki 서비스의 데이터는 제목과 본문으로 구성되며 전부 텍스트
  • 메인 페이지는Wiki 게시물 5개로 구성된 List 페이지
  • pasination 구현하기
  • 게시글을 클릭하면 detail 페이지로 이동하고 제목과 본문이 나옴
  • 메인 페이지에서 추가 버튼을 누르면 글 작성 페이지가 나옴
  • detail 페이지에서는 글 수정도 가능함

 

 

 

✔️Routing

 

페이지 구성

  • list page url : ‘/’
  • detail page url : ‘detail/postNumber’
  • create page url : ‘create/postNumber’
  • update page url : ‘update/postNumber’
<Switch>
    <Route exact path={'/'} render={()=><NoticeBoard />}/>
    <Route path={'/:page/:postId'} render={()=><BulletinBoard />} />
</Switch>

 

페이지 라우팅 과정

  • 글 생성 : list page → create 버튼 클릭 → create page → 완료 버튼 클릭 → detail page
  • 글 수정 : list page → 게시글 클릭 → detail page → 수정 버튼 클릭 → update page → 수정 완료 버튼 클릭 → detail page
  • 리스트 이동 : detail page → 리스트 페이지로 이동 클릭 → list page

 

 

 

 

✔️깨닭은 것

📢 pasination 처리 TIL : https://zibu-story.tistory.com/205

 

 

리스트 데이터 요청 및 상태관리 Reducer

처음에는 App.js(최상단 컴포넌트)에서 구현하고 props로 전달했는데 Context에서 전역으로 관리함

→ useReducer, useContext 사용

Context를 사용하면 라이브러리 설치가 없지만 목적마다 Context를 만들어야 됨

그래서 Redux나 Recoil을 사용하지만 해당 프로젝트는 규모가 작아서 내장 훅을 사용했음

 

 

App.js 관리

//App.js
const [postList, setPostList] = useState<post[]>([]) //게시글 데이터
//데이터 요청
useEffect(() =>{
	fetch('<https://jsonplaceholder.typicode.com/posts/>')
		.then((res) => res.json())
		.then((data) => setPostList(data))
},[])
//게시글 추가
const addPostList = (post:Post) => {
	setPostList([...postList,post])
}
//게시글 수정
const editPostList = (editPost:Post) => {
	setPostList(
		postList.map((post) =>
			post.id === editPost.id ? editPost : post
		)
	)
}

Context 관리

import React, {useReducer, useContext, createContext, Dispatch, useEffect} from 'react';
import {Post} from "../type/post";

interface State {
	loading: boolean,
	data: any | null,
	error: any | null,
}
//action 타입
type Action =
	|{ type: 'FETCH_INIT' }
	| { type: 'FETCH_SUCCESS', payload: Post[] }
	| { type: 'FETCH_FAILURE', payload: any }
	| {type: 'ADD_POST', payload: Post}
	| {type: 'UPDATE_POST', payload: Post}
//dispatcher 타입
type PostDispatch = Dispatch<Action>

const PostStateContext = createContext<State | null>(null);
const PostDispatchContext = createContext<PostDispatch | null>(null);

function reducer(state:State, action:Action):State {
	switch (action.type) {
		case "FETCH_INIT":
			return {...state, loading: true}
		case 'FETCH_SUCCESS' :
			return {...state, loading:false, data: action.payload, error:null}
		case "FETCH_FAILURE":
			return {...state, loading:false, data: null, error:action.payload}
		case 'ADD_POST':
			return {...state, loading:false, data: state.data.concat(action.payload), error:null}
		case 'UPDATE_POST':
			const updateData = state.data.map((post:any) =>//userId가 포함됨
				post.id === action.payload.id ? action.payload : post)
			return {...state, loading:false, data: updateData, error:null}
		default:
			throw new Error('Unhandled action')
	}
}
//컴포넌트 Provider
export function PostProvider({children}:{children:React.ReactNode}) {
	const [state, dispatch] = useReducer(reducer, {
		loading: false,
		data: null,
		error: null,
	});
	//데이터 요청
	useEffect(() =>{
		dispatch({type: 'FETCH_INIT'})
		fetch('https://jsonplaceholder.typicode.com/posts/')
			.then((res) => res.json())
			.then((data) => {dispatch({ type: 'FETCH_SUCCESS', payload: data });})
			.catch((error) =>{dispatch({ type: 'FETCH_FAILURE', payload: error });})
	},[])
	//로딩체크
	if(state.loading || !state.data) {
		return <div>Loading....</div>
	}
	//Error 체크
	if(state.error) {
		return <div>Error: {state.error.message}</div>
	}

	return (
		<PostStateContext.Provider value={state}>
			<PostDispatchContext.Provider value={dispatch}>
				{children}
			</PostDispatchContext.Provider>
		</PostStateContext.Provider>
	)
}

export function usePostState() {
	const state = useContext(PostStateContext);
	if (!state) throw new Error('Cannot find PostStateContext');
	return state;
}

export function usePostDispatch() {
	const dispatch = useContext(PostDispatchContext);
	if (!dispatch) throw new Error('Cannot find PostDispatchContext');
	return dispatch;
}

 

사용

const state = usePostState();//state 가져오기
const dispatch = usePostDispatch();//state 변경
dispatch({type:'ADD_POST', payload: newPost})

 

 

 

페이지 타입을 관리하는 enum 과 이동하는 hook 만들기

페이지 같은 경우에는 지정 되어있는 경우가 많기 때문에 enum 같은 타입으로 두면 좋고

각 컴포넌트마다 useHistory를 사용하여 페이지 이동을 할 수 없기 때문에 hook을 따로 만들었다.

 

 

enum

export enum Page {
	list='list',
	detail='detail',
	create='create',
	update='update'
}

usePageNavigation

import {Page} from "../type/page";
import {useHistory} from "react-router";

const pageUrl = (page:Page, postId?:number) =>{
	switch (page) {
		case Page.create:
			return `/${page}/${postId}`;
		case Page.detail:
			return `/${page}/${postId}`;
		case Page.update:
			return `/${page}/${postId}`;
		case Page.list:
			return '/';
	}
}

export function usePageNavigation() {
	const history = useHistory()
	const navigateTo = (page:Page,postId?:number) => {
		history.push(pageUrl(page, postId))
	};
	return {navigateTo}
}

사용

const {navigateTo} = usePageNavigation(); //선언
navigateTo(Page.detail, newPost.id) //페이지 이동 함수 사용

 

 

 

함수나 변수 캐싱, 상수는 useRef 로 관리

📢 참고 : https://zibu-story.tistory.com/203

 

useState만 사용하면 state가 변경될 때마다 화면이 랜더링 되고 컴포넌트 자체가 함수이기 때문에 호출 시(랜더링 시) 함수와 변수가 재 할당 된다. 이 부분을 해결하기 위해 React는 useRef나 useCallback, useMemo 등을 제공해준다. 그리고 useEffect 훅은 요청 로직을 제외하고는 가급적 사용하지 않는 것이 좋다.

//페이지 그룹 ex)1, 2, 3, 4, 5 -> 1그룹
const pageGroup = useRef<number>(1)
//props로 넘기는 페이지 변경 함수
const paginate = useCallback((pageNumber:number):void => {setCurrentPage(pageNumber)},[])

 

 

 

 

 

 

 

 

 

 

 

 

 

반응형