최근엔 제작중이던 홈페이지의 게시판을 구현하고 있다.
하나의 페이지에 총 5개의 게시판이 등장하는 형태이고, 각각의 게시판마다 더보기 버튼을 클릭하면 해당 게시판의 전체 글을 볼 수 있도록 계획했다.
처음엔 완성하는데 1주일 정도 걸릴 것이라 예상했지만 게시판이라는 것이 생각보다 신경써야 할 부분이 많았고, 시간이 꽤 많이 지나버렸다. 😂
아직까지도 디테일한 기능은 부족하지만 이 부분은 계속 보완할 예정이다.
그리고 이번 포스팅은 지금까지 구현한 모든 내용을 다 다루면 너무 길어지기 때문에 위 이미지의 '정기 모임' 게시판에 글을 올리고 수정, 삭제하는 것을 중점으로 다루려 한다.
게시판의 기본 기능
게시판의 필수적으로 CURD 기능이 구현되어야 한다. 작성자가 글을 쓰고(Create), 수정(Update)을 하면 화면에 내용이 나타난다.(Read) 추후 글 작성자가 원할 땐 글을 삭제(Delete)할 수 있어야 한다.
게시판 구현에 필요한 페이지
게시판 구현을 위해선 기본적으로 유저들이 작성할 글을 표시할 리스트 페이지, 글을 작성하고 수정할 에디터 페이지, 완성된 글을 확인할 뷰 페이지가필요하다.
리스트 페이지와 뷰 페이지에선 axios 통신을 통해 서버를 거쳐 DB에 있는 데이터를 불러와 마크업과 함께 화면에 뿌리고, 에디터 페이지에선 작성한 글을 DB에 저장하거나 특정 글을 수정할 것이다.
리스트(List) 페이지
게시글들을 모아둔 페이지로, 각 리스트에는 글의 제목, 작성자의 닉네임, 소유한 차량을 표시한다.
물론 DB에 저장된 정보 중 작성 일자 등을 추가로 표시할 수도 있다.
DB에서 불러온 배열 형태의 글 목록 데이터를 map() 매서드로 렌더링하고, Link를 통해 클릭 시 이동할 경로를 지정해주었다.
즉, 뷰 페이지로 연결되게 하면 된다.
// 리스트 페이지
import { useEffect, useState } from 'react';
import axios from 'axios';
const LifestyleList=()=>{
//useParams로 게시판 이름 구분
let boardName='정기 모임';
// 현재 게시판의 데이터를 저장할 state
const [boardList, setBoardList]=useState(null);
// DB에서 데이터를 불러온 후 다시 렌더링
useEffect(()=>{
axios.get(`/api/board/${boardName}`).then((res)=>{
const loadData=res.data;
setBoardList(loadData);
}).catch((err)=>{
// 에러 처리
});
}, []);
if(!boardList){
return <p>작성된 글이 없습니다.</p>
}
else{
return (
<ul className='post_list'>
{boardList.reverse().map((item)=>
<li key={item.id} className='post_item'>
<Link to={`/brand/lifestyle/${boardName}/${item.id}`} state={item}>
<ul>
<li><span className='title'>{item.title}</span></li>
<li><span className='ninckname'>{item.nickname}</span> <span className='car'>{item.car}</span></li>
</ul>
</Link>
</li>
)}
</ul>
);
}
}
export default LifestyleList;
여기서 언급하고 싶은 내용은 axios 통신을 useEffect Hook과 함께 사용했다는 점이다.
처음엔 단순히 마크업보다 DB 데이터를 먼저 불러오면 되겠다는 생각으로 LifrstyleList 컴포넌트 위에 axios 코드를 작성했고, 실제로 잘 작동하는 듯 보였다.
하지만 다른 기능을 구현하던 중 뭔가 이상함을 감지하고 새로고침을 연타해보았더니 렌더링 오류가 날 때도 있고 정상적으로 표시될 때도 있었다.
아마 남은 캐시가 있거나 정말 운이 좋게 DB 로딩이 먼저 될 경우 정상적으로 표시되는것 같았는데, 이는 분명히 잘못된 상황이다.
해결방법
비동기 통신이 완료된 후의 최종 상태를 반영하기 위해 useState와 useEffect Hook을 사용해 DB에서 불러온 데이터를 최신 상태로 관리하면 된다.
이러면 로딩 상황에 따라 자동으로 리렌더링이 진행되기 때문에 로딩이 끝나면 자동으로 업데이트된 목록이 화면에 출력된다.
그런데 useEffect를 꼭 써야 하나?
이 부분은 포스팅 작성 중 즉흥적으로 든 의문인데, 생각해본 결과 렌더링이 진행되는 중에 통신이 완료될 경우 상태가 정확히 업데이트 되지 않을 가능성도 있어보인다.
워낙 짧은 순간일 것이라 실제로 영향이 있을지는 정확한 테스트를 해봐야 알겠지만 아무래도 렌더링이 완전히 끝난 후에 상태를 다시 업데이트 하는 편이 기능적으로 안정적일 것이라 생각된다.
에디터(Editor) 페이지
리스트 페이지 하단의 글쓰기 버튼을 누르면 에디터 페이지로 연결되도록 만들었다.
이 페이지는 로그인을 완료한 회원만 접속이 가능해야 하므로, 로그인하지 않은 유저가 접근한다면 로그인 페이지로 우선 이동시켜주어야 한다.
유저의 로그인 여부를 판단하고 서버에 데이터를 전달하는 과정에서 유저의 ID, 닉네임 등의 정보가 필요하기 때문에 LoginUserInfoContext를 통해 확인 가능한 일부 정보를 input 상태에 미리 저장해둔다.
이 데이터는 유저가 따로 작성하지 않아도 글을 올리면 알아서 서버에 전달되어야 하기 때문이다.
// 에디터 페이지
import { useState, useContext, useEffect } from "react";
import { useNavigate } from "react-router-dom";
import { LoginUserInfoContext } from "../../../App";
import axios from "axios";
const LifestyleEditor=()=>{
const nav=useNavigate();
// 로그인한 유저의 정보
const {loginUserInfo}=useContext(LoginUserInfoContext);
// 게시판 이름, DB 통신에 필요
const boardName='정기 모임';
// 로그인 하지 않은 유저가 접근할 때
useEffect(()=>{
if(!loginUserInfo.isLogin){
if(confirm('로그인이 필요합니다!')){
nav('/login');
return;
}
else{
nav('/brand/lifestyle');
return;
}
}
}, []);
// input 기본 값.
// 로그인한 유저의 정보와 게시판 이름까지 서버에 전달해 적절한 DB에 저장
const initInput={
title:'',
userId:loginUserInfo.userId,
nickname:loginUserInfo.nickname,
car:loginUserInfo.car,
content:'',
};
const [input, setInput]=useState(initInput);
const onChangeInput=(e)=>{
const newInput={
...input,
[e.target.name]:e.target.value
}
setInput(newInput);
}
// 초기화 버튼
const onClickInit=()=>{setInput(initInput);}
// 완료 버튼
const onClickSubmit=()=>{
axios.get('/api/board/post_submit', {params:input})
.then(()=>{
alert('글 작성이 완료되었습니다.');
nav(`/brand/lifestyle/${boardName}`);
}).catch(()=>{
alert('오류가 발생했습니다!');
});
}
return (
<form>
<ul>
<li>
<label htmlFor="title">제목</label>
:
<input id="title" name="title" type="text" value={input.title} onChange={onChangeInput} />
</li>
<li>
<label htmlFor="content">내용</label>
:
<textarea id="content" name="content" value={input.content} onChange={onChangeInput} />
</li>
</ul>
<div className='button_area'>
<button type='button' onClick={onClickInit}>초기화</button>
<button type='button' onClick={onClickSubmit}>완료</button>
</div>
</form>
);
}
export default LifestyleEditor;
작성이 완료되면 완성 버튼을 눌러 post_submit api를 호출하면 된다.
해당 api는 DB에 데이터를 추가하는 Node.js 코드를 실행시켜 실제로 DB에 입력값을 저장시킨다.
이전에 작성한 포스팅을 참고하면 어렵지 않게 작성할 수 있다.
https://duski96.tistory.com/20
[Node.js] DB에 입력 값 추가하기
홈페이지 방문자가 회원 가입을 진행한다면 입력한 데이터는 DB에 저장된다.이번 포스팅은 DB에서 데이터를 불러오지 않고 반대로 데이터를 추가하는 방법을 다룰 것이다.사실 이 부분은 SQL문만
duski96.tistory.com
다만 차이가 있다면 클라이언트에서 get() 매서드로 호출을 했기 때문에 Node.js에서 req.body가 아닌 req.query로 데이터를 전달받는다.
참고로 아직 게시판의 기능이 미흡해 제목과 내용에 텍스트 정도밖에 적을 수 없으나, 추후 이미지 업로드까지 가능하게 만드는게 목표이다.
뷰(View) 페이지
뷰 페이지에선 글의 제목, 유저의 닉네임, 차량 외에 본문과 작성일자가 표시된다.
데이터를 불러오는 방식은 리스트 페이지와 크게 다르지 않고, 한 페이지에 하나의 글만 보여야 하므로 filter() 매서드를 활용해 사용자가 클릭한 제목의 id에 해당하는 데이터를 보여주면 된다.
추가로 수정 / 삭제 버튼은 로그인한 유저와 작성자가 일치할 경우에만 화면에 보이도록 className을 지정해 CSS 코드를 작성했고, 만약을 대비해 작성자와 유저가 다를 경우 해당 버튼을 누르면 권한이 없음을 알리는 경고창이 뜨도록 했다.
// 뷰 페이지
import { useContext, useEffect, useState } from "react";
import { useParams, useNavigate } from "react-router-dom";
import { LoginUserInfoContext } from "../../../App";
import axios from "axios";
const LifestyleViewer=()=>{
// 게시판 이름
const boardName='정기 모임';
const nav=useNavigate();
// 로그인 여부 확인 후 수정 및 삭제 가능
const {loginUserInfo}=useContext(LoginUserInfoContext);
// 현재 포스트 아이디
const curId=useParams().id;
// 현재 데이터 담을 state
const [curData, setCurData]=useState(null);
// 로딩이 끝나고 다시 렌더링
useEffect(()=>{
axios.get(`/api/board/${boardName}`).then((res)=>{
const loadData=res.data.filter((item)=>item.id===Number(curId))[0];
setCurData(loadData);
}).catch((err)=>{
// 에러 처리
});
}, []);
// DB 데이터 로딩 이전
if(!curData){
return <p>로딩중입니다.</p>
}
// 목록으로 버튼
const onClickList=()=>{
nav(`/brand/lifestyle/${boardName}`);
}
// 수정 버튼
const onClickEdit=()=>{
// 로그인 하지 않거나 권한이 없으면 수정 불가
if(!loginUserInfo)
return;
if(loginUserInfo.userId!==curData.user_id){
alert('수정 권한이 없습니다.');
return;
}
else{
nav(`/brand/lifestyle/${boardName}/editor`, {state:{value:curData}});
}
}
const onClickDelete=()=>{
// 로그인 하지 않거나 권한이 없으면 수정 불가
if(!loginUserInfo)
return;
if(loginUserInfo.userId!==curData.user_id){
alert('삭제 권한이 없습니다.');
return;
}
else{
axios.get('/api/board/delete', {params:{id:curData.id, userId:curData.user_id, board:boardName}})
.then(()=>{
alert('삭제되었습니다.');
nav(`/brand/lifestyle/${boardName}`)
}).catch(()=>{
alert('삭제할 수 없습니다.')
});
}
}
return(
<h4 className='page_title'>{curData.title}</h4>
<div className='write_info'>
<div className='user_info'>
<span className='ninckname'>{curData.nickname}</span> <span className='car'>{curData.car}</span>
</div>
<span className='created_date'>작성일자 : {curData.created_date.slice(0, 19).replace('T', ' ')}</span>
</div>
<hr />
<div className='content'>
<p>
{curData.content}
</p>
</div>
<div className='button_area'>
<button type='button' className='visible' onClick={onClickList}>목록으로</button>
<button type='button' className={`edit ${loginUserInfo.userId===curData.user_id ? 'visible': ''}`} onClick={onClickEdit}>수정</button>
<button type='button' className={`delete ${loginUserInfo.userId===curData.user_id ? 'visible': ''}`} onClick={onClickDelete}>삭제</button>
</div>
);
}
export default LifestyleViewer;
리스트 페이지와 마찬가지로 useState와 useEffect를 사용해 DB로부터 불러온 데이터가 최신상태일 때 렌더링을 진행한다.
참고로 id로 필터링을 하던 과정에서 계속 오류가 났는데 알고 보니 useParams를 통해 불러온 id가 문자열 형태였고, 이를 Number(curId)를 통해 숫자 비교가 가능한 상태로 만들었더니 정상적으로 필터링되었다.
수정 버튼을 클릭할 경우 현재 보고있는 글의 내용(데이터)을 에디터 페이지로 가져가기 위해 네비게이터에 {state:value} 형태의 객체를 추가로 담아주었다.
에디터 페이지에 아래 코드를 추가하고 initInput 객체를 수정하면 마지막으로 글이 작성된 상태에서 이어서 수정이 가능하다.
// 에디터 페이지 코드에 추가
import { useLocation } from react-router-dom;
// 유저가 수정 버튼을 눌러서 들어오면 기존 data를 전달받음
const linkData=useLocation().state;
const initInput={
id:linkData ? linkData.value.id : undefined,
title:linkData ? linkData.value.title : '',
userId:loginUserInfo.userId,
nickname:loginUserInfo.nickname,
car:loginUserInfo.car,
content:linkData ? linkData.value.content : '',
};
삭제 버튼을 클릭하면 해당 동작에 맞는 api를 호출해야 하는데, 파라미터로 sql 쿼리문에 사용하기 위한 id, userId 속성을 전달해주어야 한다.
id 하나만 전달해도 되긴 하지만 좀 더 안전한 동작을 위해 두 가지의 수정 불가능한 속성을 조건으로 부여하기 위함이다.
P.S.
사실 완성된 코드는 여러 조건도 붙어있고 추가된 Hook도 있기 때문에 포스팅에 적은 코드보다 훨씬 길긴 하다.
그래서 이번 포스팅은 개인적으로 헤맸던 부분이나 중요하다고 생각되는 요소들 위주로 포스팅했다.
api 구현 관련해서는 위에 링크 걸었던 'DB에 입력값 추가하기' 포스팅에서 sql문만 조금씩 수정하면 되는 수준이라 따로 작성하진 않았다.
'React' 카테고리의 다른 글
[React] Vercel에서 리액트 앱 배포하기 (0) | 2025.03.19 |
---|---|
[React] 회원 가입 폼 구현하기 (0) | 2025.03.05 |
[React] 로그인 구현하기 (0) | 2025.03.02 |
[React] React에 MySQL 연동하기 (0) | 2025.02.25 |
[React] React에서 Fancybox 사용하기 (0) | 2025.02.20 |