1. 파일 만들기

필요한 라이브러리
npm install
- axios
- react-redux
- jwt-decode
- bootstrap
- react-bootstrap
- react-bootstrap-icons
- styled-components
- react-router-dom
2. 페이지 작성
- 통신 전에는 더미 데이터를 이용한 화면 구성만 작성
JoinForm
import axios from "axios";
import React, { useState } from "react";
import { Button, Form } from "react-bootstrap";
import { useNavigate } from "react-router-dom";
const JoinForm = (props) => {
const navigate = useNavigate();
const [user, setUser] = useState({});
function changeValue(e) {}
async function submitJoin(e) {
e.preventDefault(); // 새로고침 막기 (action 발동 막기)
}
return (
<Form>
<Form.Group>
<Form.Label>Username</Form.Label>
<Form.Control
type="text"
placeholder="Enter username"
name="username"
onChange={changeValue}
/>
</Form.Group>
<Form.Group>
<Form.Label>Password</Form.Label>
<Form.Control
type="password"
placeholder="Enter password"
name="password"
onChange={changeValue}
/>
</Form.Group>
<Form.Group>
<Form.Label>Email</Form.Label>
<Form.Control
type="email"
placeholder="Enter email"
name="email"
onChange={changeValue}
/>
</Form.Group>
<Button variant="primary" type="submit" onClick={submitJoin}>
회원가입
</Button>
</Form>
);
};
export default JoinForm;
LoginForm
import axios from "axios";
import React, { useState } from "react";
import { Button, Form } from "react-bootstrap";
import { useDispatch } from "react-redux";
import { useNavigate } from "react-router-dom";
const LoginForm = (props) => {
const navigate = useNavigate();
const [user, setUser] = useState({});
async function submitLogin(e) {
e.preventDefault();
}
const changeValue = (e) => {};
return (
<Form>
<Form.Group>
<Form.Label>Username</Form.Label>
<Form.Control
type="text"
placeholder="Enter username"
name="username"
onChange={changeValue}
/>
</Form.Group>
<Form.Group>
<Form.Label>Password</Form.Label>
<Form.Control
type="password"
placeholder="Enter password"
name="password"
onChange={changeValue}
/>
</Form.Group>
<Button variant="primary" type="submit" onClick={submitLogin}>
로그인
</Button>
</Form>
);
};
export default LoginForm;
Home
import React, { useState } from "react";
import { Form, FormControl, Pagination } from "react-bootstrap";
import BoardItem from "../../components/BoardItem";
const Home = () => {
const [page, setPage] = useState(0);
const [keyword, setKeyword] = useState("");
const [model, setModel] = useState({});
async function apiHome() {}
function changeValue(e) {}
return (
<div>
<Form className="d-flex mb-4" onSubmit={""}>
<FormControl
type="search"
placeholder="Search"
className="me-2"
aria-label="Search"
value={keyword}
onChange={changeValue}
/>
</Form>
<BoardItem key={1} id={1} title={"제목1"} page={0} />
<br />
<div className="d-flex justify-content-center">
<Pagination>
<Pagination.Item onClick={""} disabled={false}>
Prev
</Pagination.Item>
<Pagination.Item onClick={""} disabled={false}>
Next
</Pagination.Item>
</Pagination>
</div>
</div>
);
};
export default Home;
Detail
import React, { useState } from "react";
import { Button } from "react-bootstrap";
import { Link, useNavigate } from "react-router-dom";
const Detail = (props) => {
const { id } = 1;
const navigate = useNavigate();
const [board, setBoard] = useState({});
async function fetchDetail(boardId) {}
async function fetchDelete(boardId) {}
return (
<div>
<Link to={`/update-form/${board.id}`} className="btn btn-warning">
수정
</Link>
<Button className="btn btn-danger" onClick={() => fetchDelete(board.id)}>
삭제
</Button>
<br />
<br />
<h1>제목1</h1>
<hr />
<div>내용1</div>
</div>
);
};
export default Detail;
SaveForm
import axios from "axios";
import React, { useState } from "react";
import { Button, Form } from "react-bootstrap";
import { useSelector } from "react-redux";
import { useNavigate } from "react-router-dom";
const SaveForm = (props) => {
const navigate = useNavigate();
const [board, setBoard] = useState({});
async function submitPost(e) {
e.preventDefault();
}
function changeValue(e) {}
return (
<div>
<h1>글쓰기</h1>
<hr />
<Form>
<Form.Group>
<Form.Label>Title</Form.Label>
<Form.Control
type="text"
placeholder="Enter title"
name="title"
onChange={changeValue}
/>
</Form.Group>
<Form.Group>
<Form.Label>Content</Form.Label>
<Form.Control
as="textarea"
row={5}
name="content"
onChange={changeValue}
/>
</Form.Group>
<Button variant="primary" type="submit" onClick={submitPost}>
글등록
</Button>
</Form>
</div>
);
};
export default SaveForm;
UpdateForm
import axios from "axios";
import React, { useEffect, useState } from "react";
import { Button, Form } from "react-bootstrap";
import { useSelector } from "react-redux";
import { useNavigate, useParams } from "react-router-dom";
const UpdateForm = (props) => {
const { id } = 1;
const navigate = useNavigate();
const [board, setBoard] = useState({});
async function updateSubmit(e) {
e.preventDefault();
}
const changeValue = (e) => {};
async function fetchUserInfo() {}
return (
<div>
<h1>글수정</h1>
<hr />
<Form>
<Form.Group>
<Form.Label>Title</Form.Label>
<Form.Control
value={"제목1"}
type="text"
placeholder="Enter title"
name="title"
onChange={changeValue}
/>
</Form.Group>
<Form.Group>
<Form.Label>Content</Form.Label>
<Form.Control
as="textarea"
row={5}
value={"내용1"}
name="content"
onChange={changeValue}
/>
</Form.Group>
<Button variant="primary" type="submit" onClick={updateSubmit}>
글수정
</Button>
</Form>
</div>
);
};
export default UpdateForm;
3. 컴포넌트 작성
BoardItem
import React from "react";
import { Card } from "react-bootstrap";
import { Link } from "react-router-dom";
const BoardItem = (props) => {
return (
<Card className="mb-2">
<Card.Body>
<Card.Title>제목1</Card.Title>
<Link to={"/board/1"} variant="primary" className="btn btn-primary">
상세보기
</Link>
</Card.Body>
</Card>
);
};
export default BoardItem;
Header
import React from "react";
import { Nav, Navbar } from "react-bootstrap";
import { Link } from "react-router-dom";
function Header(props) {
return (
<div>
<Navbar bg="dark" expand="lg" variant="dark">
<Link to="/" className="navbar-brand">
블로그홈
</Link>
<Navbar.Toggle aria-controls="basic-navbar-nav" />
<Navbar.Collapse id="basic-navbar-nav">
<Nav className="mr-auto">
<Link to="/save-form" className="nav-link">
글쓰기
</Link>
<Link className="nav-link" onClick={""}>
로그아웃
</Link>
<Link to="/login-form" className="nav-link">
로그인
</Link>
<Link to="/join-form" className="nav-link">
회원가입
</Link>
</Nav>
</Navbar.Collapse>
</Navbar>
<br />
</div>
);
}
export default Header;
4. 파일 수정
index.js
- App을 BrowserRouter로 감쌈 : 주소로 이동 가능
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
import { BrowserRouter } from "react-router-dom";
import "bootstrap/dist/css/bootstrap.min.css";
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<BrowserRouter>
<App />
</BrowserRouter>
);
초기 화면






5. 로그인/로그아웃
JoinForm
- changeValue, submitJoin 작성
- changeValue : 값이 바뀔 때 user 객체 내부 멤버 값을 update
- submitJoin : 기존 action 발동을 막고, axios를 통한 통신 진행
- body를 JS Object에서 JSON으로 자동 변환
- 에러가 나면 터짐 : try-catch로 잡아줘야 함
- 가입 완료 시 login-form으로 이동
import axios from "axios";
import React, { useState } from "react";
import { Button, Form } from "react-bootstrap";
import { useNavigate } from "react-router-dom";
const JoinForm = (props) => {
const navigate = useNavigate();
const [user, setUser] = useState({
username: "",
password: "",
email: "",
});
function changeValue(e) {
setUser({
...user,
[e.target.name]: e.target.value,
});
}
async function submitJoin(e) {
e.preventDefault(); // 새로고침 막기 (action 발동 막기)
try {
await axios({
method: "POST",
url: "http://localhost:8080/join",
data: user, // axios는 JS Object를 전달하면 JSON으로 변환해서 전달
headers: {
"Content-Type": "application/json",
},
});
navigate("/login-form");
} catch (error) {
// console.log(error);
alert(error.response.data.msg);
}
}
// console.log(user);
return (
<Form>
<Form.Group>
<Form.Label>Username</Form.Label>
<Form.Control
type="text"
placeholder="Enter username"
name="username"
onChange={changeValue}
/>
</Form.Group>
<Form.Group>
<Form.Label>Password</Form.Label>
<Form.Control
type="password"
placeholder="Enter password"
name="password"
onChange={changeValue}
/>
</Form.Group>
<Form.Group>
<Form.Label>Email</Form.Label>
<Form.Control
type="email"
placeholder="Enter email"
name="email"
onChange={changeValue}
/>
</Form.Group>
<Button variant="primary" type="submit" onClick={submitJoin}>
회원가입
</Button>
</Form>
);
};
export default JoinForm;
- username 중복 시 400 에러 발생


LoginForm
- useDispatch : 전역 상태 변경 → re-render
- changeValue는 똑같이 작성
- submitLogin
- 응답으로 넘어오는 jwt를 local storage에 저장 : 브라우저를 꺼도 사라지지 않음 (만료 전까지)
- 자동 로그인 사용 가능
- dispatch로 상태 변경 → reducer 호출
import axios from "axios";
import React, { useState } from "react";
import { Button, Form } from "react-bootstrap";
import { useDispatch } from "react-redux";
import { useNavigate } from "react-router-dom";
import { login } from "../../store";
const LoginForm = (props) => {
const navigate = useNavigate();
const dispatch = useDispatch(); // reducer 호출
const [user, setUser] = useState({
username: "",
password: "",
});
async function submitLogin(e) {
e.preventDefault();
try {
let response = await axios({
method: "POST",
url: "http://localhost:8080/login",
data: user, // axios는 JS Object를 전달하면 JSON으로 변환해서 전달
headers: {
"Content-Type": "application/json",
},
});
let jwt = response.headers.authorization;
localStorage.setItem("jwt", jwt);
dispatch(login(jwt));
navigate("/");
} catch (error) {
// console.log(error);
alert(error.response.data.msg);
}
}
const changeValue = (e) => {
// 유효성 검사
setUser({
...user,
[e.target.name]: e.target.value,
});
};
return (
<Form>
<Form.Group>
<Form.Label>Username</Form.Label>
<Form.Control
type="text"
placeholder="Enter username"
name="username"
onChange={changeValue}
/>
</Form.Group>
<Form.Group>
<Form.Label>Password</Form.Label>
<Form.Control
type="password"
placeholder="Enter password"
name="password"
onChange={changeValue}
/>
</Form.Group>
<Button variant="primary" type="submit" onClick={submitLogin}>
로그인
</Button>
</Form>
);
};
export default LoginForm;
- 브라우저의 저장소
- Cookies : 데이터 및 인증 정보 등을 저장할 때 사용, 브라우저 종료 후에도 만료일에 따라 데이터 유지 가능
- Local Storage : 데이터를 영구적으로 저장 가능 (자동 로그인에 사용 가능)
- Session Storage : Local Storage와 유사하나 브라우저를 끄면 삭제
- IndexedDB : 대용량의 데이터를 저장할 수 있는 NoSQL 데이터베이스


Header
- useDispatch : 전역 상태 관리
- isLogin : 로그인 여부 → useSelector를 이용해 state의 isLogin을 주시
- 로그아웃 시
- 로컬 스토리지의 jwt 제거
- dispatch(logout()) : 로그아웃 시 상태 변경
- navbar에서 isLogin 값에 따라 태그 표시 : 삼항연산자 사용
import React from "react";
import { Nav, Navbar } from "react-bootstrap";
import { useDispatch, useSelector } from "react-redux";
import { Link } from "react-router-dom";
import { logout } from "../store";
function Header(props) {
const dispatch = useDispatch();
const isLogin = useSelector((state) => state.isLogin);
async function logoutEvent() {
await localStorage.removeItem("jwt");
dispatch(logout());
}
return (
<div>
<Navbar bg="dark" expand="lg" variant="dark">
<Link to="/" className="navbar-brand">
블로그홈
</Link>
<Navbar.Toggle aria-controls="basic-navbar-nav" />
<Navbar.Collapse id="basic-navbar-nav">
<Nav className="mr-auto">
{isLogin ? (
<>
<Link to="/save-form" className="nav-link">
글쓰기
</Link>
<Link className="nav-link" onClick={logoutEvent}>
로그아웃
</Link>
</>
) : (
<>
<Link to="/login-form" className="nav-link">
로그인
</Link>
<Link to="/join-form" className="nav-link">
회원가입
</Link>
</>
)}
</Nav>
</Navbar.Collapse>
</Navbar>
<br />
</div>
);
}
export default Header;
index.js
- reducer 사용 : 전역 상태 관리를 위해 필요
- Provider : 전역 상태를 담을 창고 설정 = gvm → store
import { configureStore } from "@reduxjs/toolkit";
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
import { BrowserRouter } from "react-router-dom";
import "bootstrap/dist/css/bootstrap.min.css";
import { Provider } from "react-redux";
import reducer from "./store";
const root = ReactDOM.createRoot(document.getElementById("root"));
const store = configureStore({
reducer: reducer,
});
root.render(
<Provider store={store}>
<BrowserRouter>
<App />
</BrowserRouter>
</Provider>
);
store.js
- 전역 상태를 관리하는 JS (riverpod의 GVM과 역할이 비슷함)
- initialState : 초기 상태 → 로그인 안됨, 토큰 없음
- reducer 설정
- 로그인 및 로그아웃 시 상태 변경
- action = reducer를 호출하는 함수
- 로그인 시 : isLogin = true, jwt = action.jwt
- 로그아웃 시 : isLogin = false, jwt = null (빈칸)
- action 설정
- login, logout
// 전역 상태 관리 (user)
// 1. 초기 상태
const initialState = {
isLogin: false,
jwt: "",
};
// 2. reducer
const reducer = (state = initialState, action) => {
switch (action.type) {
case "LOGIN":
return {
isLogin: true,
jwt: action.jwt,
};
case "LOGOUT":
return {
isLogin: false,
jwt: "",
};
default:
return state;
}
};
// 3. action
export function login(jwt) {
return {
type: "LOGIN",
jwt: jwt,
};
}
export function logout() {
return {
type: "LOGOUT",
};
}
export default reducer;
변경된 화면
- 비 로그인 시 : 로그인, 회원가입 메뉴 표시
- 로그인 시 : 글쓰기, 로그아웃 메뉴 표시


- 회원가입 시 username이 중복 시 alert 발생

6. 페이징 & 검색
Home
- model 정의 : DTO에 들어오는 데이터에 맞추어서 바인딩
- useEffect : state가 변경되면 처리할 함수 정의
- prev, next : 페이지 갱신
- isFirst가 true이면 prev를 비활성화
- isLast가 true이면 next를 비활성화
- changeValue : 검색 창에 키워드 입력 시 키워드 변경
- 키워드가 변경되지 않으면 글이 입력되지 않음 → DOM이 변경을 감지할 수 없음
- 상태가 변경된 model에서 boards만 골라 출력 : model.boards.map()
import React, { useEffect, useState } from "react";
import { Form, FormControl, Pagination } from "react-bootstrap";
import BoardItem from "../../components/BoardItem";
import axios from "axios";
const Home = () => {
const [page, setPage] = useState(0);
const [keyword, setKeyword] = useState("");
const [model, setModel] = useState({
totalPage: undefined,
number: 0,
isFirst: true,
isLast: false,
boards: [],
});
useEffect(() => {
apiHome();
}, [page, keyword]);
function prev() {
setPage(page - 1);
}
function next() {
setPage(page + 1);
}
async function apiHome() {
let response = await axios({
method: "get",
url: `http://localhost:8080?page=${page}&keyword=${keyword}`,
});
console.log(response.data.body);
setModel(response.data.body);
}
function changeValue(e) {
setKeyword(e.target.value);
}
return (
<div>
<Form className="d-flex mb-4" onSubmit={""}>
<FormControl
type="search"
placeholder="Search"
className="me-2"
aria-label="Search"
value={keyword}
onChange={changeValue}
/>
</Form>
{model.boards.map((board) => (
<BoardItem key={board.id} id={board.id} title={board.title} page={0} />
))}
<br />
<div className="d-flex justify-content-center">
<Pagination>
<Pagination.Item onClick={prev} disabled={model.isFirst}>
Prev
</Pagination.Item>
<Pagination.Item onClick={next} disabled={model.isLast}>
Next
</Pagination.Item>
</Pagination>
</div>
</div>
);
};
export default Home;
BoardItem
- board의 카드 내용 변경
- id, title을 받아서 바인딩
- 링크는 백틱(``)을 이용하여 키워드 바인딩 → 주소는 문자열이기 때문
import React from "react";
import { Card } from "react-bootstrap";
import { Link } from "react-router-dom";
const BoardItem = (props) => {
const { id, title } = props;
return (
<Card className="mb-2">
<Card.Body>
<Card.Title>{title}</Card.Title>
<Link to={`/board/${id}`} variant="primary" className="btn btn-primary">
상세보기
</Link>
</Card.Body>
</Card>
);
};
export default BoardItem;
6-1. 검색 디바운싱 적용
Home
- 키가 입력될 때마다 useEffect 발동 : 너무 잦은 호출
- 이를 최적화하기 위한 디바운싱 (Debouncing) 적용
- rawKeyword : 검색창에 키워드를 입력할 때 이를 확인하기 위한 키워드 상태
- keyword : 실제 호출에 사용할 키워드
- useRef : 상태를 변경하지만 추가 행동을 하지 않음 → 값이 변경되어도 렌더링 X
- useRef 내부의 ref.current에 해당 상태 값 저장
- 값을 바꿔도 렌더링 X, 리렌더링이 되어도 값 그대로 유지
- _.debounce : 외부 라이브러리 lodash에서 지원하는 디바운스 함수
- 호출할 함수, 딜레이 설정
- useEffect를 2번 사용 : 취소 시, 키워드 변경 시
- 취소 시 타이머 적용 취소 → 키 연속 입력 시 타이머 갱신 용
- 키워드가 정상적으로 변경 시 새 호출
- cancelToken : axios 내부에서 사용할 수 있는 요청 취소 토큰 (≠ JWT)
- 이전 검색 요청을 버리는 용도
import React, { useEffect, useRef, useState } from "react";
import { Form, FormControl, Pagination } from "react-bootstrap";
import BoardItem from "../../components/BoardItem";
import axios from "axios";
import _ from "lodash";
const Home = () => {
const [page, setPage] = useState(0);
// 1) 입력 즉시 DOM 반영용
const [rawKeyword, setRawKeyword] = useState("");
// 2) 요청 트리거용(디바운스 후 반영)
const [keyword, setKeyword] = useState("");
const [model, setModel] = useState({
totalPage: undefined,
number: 0,
isFirst: true,
isLast: false,
boards: [],
});
// --- 디바운스된 setter: 최초 1회 생성, 언마운트 시 cancel ---
const debouncedSetKeyword = useRef(
_.debounce((value) => {
setKeyword(value); // 디바운스 후에만 서버요청 트리거 상태 변경
setPage(0); // 새 키워드면 페이지를 0으로 리셋 (옵션)
}, 600) // 3초가 너무 길다면 600~800ms 권장
);
useEffect(() => {
return () => {
// 컴포넌트 언마운트 시 디바운스 타이머 취소
debouncedSetKeyword.current.cancel();
};
}, []);
// --- 서버 호출: page, keyword(디바운스된 값) 변경 시에만 ---
useEffect(() => {
const source = axios.CancelToken.source();
(async () => {
try {
const res = await axios.get("http://localhost:8080", {
params: { page, keyword },
cancelToken: source.token,
});
setModel(res.data.body);
} catch (e) {
if (!axios.isCancel(e)) {
console.error(e);
}
}
})();
// 의존성 변경/언마운트 시 이전 요청 취소
return () => source.cancel("request aborted due to new search");
}, [page, keyword]);
function prev() {
setPage((p) => p - 1);
}
function next() {
setPage((p) => p + 1);
}
// --- 입력은 즉시 rawKeyword에 반영, 서버요청은 디바운스 ---
function changeValue(e) {
const value = e.target.value;
setRawKeyword(value); // 즉시 DOM 갱신
debouncedSetKeyword.current(value); // 요청은 지연
}
return (
<div>
<Form className="d-flex mb-4" onSubmit={(e) => e.preventDefault()}>
<FormControl
type="search"
placeholder="Search"
className="me-2"
aria-label="Search"
value={rawKeyword} // ✅ 입력은 즉시 보이도록 rawKeyword 바인딩
onChange={changeValue}
/>
</Form>
{model.boards.map((board) => (
<BoardItem key={board.id} id={board.id} title={board.title} page={0} />
))}
<br />
<div className="d-flex justify-content-center">
<Pagination>
<Pagination.Item onClick={prev} disabled={model.isFirst}>
Prev
</Pagination.Item>
<Pagination.Item onClick={next} disabled={model.isLast}>
Next
</Pagination.Item>
</Pagination>
</div>
</div>
);
};
export default Home;
- useEffect로만 구현도 가능 = setTimeout, clearTimeout으로 구현 가능
- lodash를 useEffect만 사용해서 디바운싱 구현은 어려움
- lodash의 debounce는 내부 함수에 사용할 value를 저장하고 일정 시간 후 내부 함수 호출
- 리렌더링 시 lodash가 새로 호출되어 버려짐 : 이전 요청의 함수들은 버려짐
- 검색을 하게 되면 useEffect로 인해 화면이 리렌더링 → debounce 함수 복제 → 이전 명령 삭제로 인한 검색 불가
- lodash를 쓸 때에는 리렌더링이 되어도 해당 함수의 상태를 유지할 수 있도록 useMemo 또는 useRef를 사용해야 함
import React, { useEffect, useRef, useState } from "react";
import { Form, FormControl, Pagination } from "react-bootstrap";
import BoardItem from "../../components/BoardItem";
import axios from "axios";
import _ from "lodash";
const Home = () => {
const [page, setPage] = useState(0);
// 1) 입력 즉시 DOM 반영용
const [rawKeyword, setRawKeyword] = useState("");
// 2) 요청 트리거용(디바운스 후 반영)
const [keyword, setKeyword] = useState("");
const [model, setModel] = useState({
totalPage: undefined,
number: 0,
isFirst: true,
isLast: false,
boards: [],
});
// 🔹 rawKeyword가 바뀔 때마다 600ms 뒤에 keyword 반영 (디바운스)
useEffect(() => {
const t = setTimeout(() => {
setKeyword(rawKeyword);
}, 600);
return () => clearTimeout(t); // 입력이 다시 들어오면 이전 타이머 취소
}, [rawKeyword]);
useEffect(() => {
apiHome();
}, [page, keyword]);
async function apiHome() {
let response = await axios({
method: "get",
url: `http://localhost:8080?page=${page}&keyword=${keyword}`,
});
let responseBody = response.data;
setModel(responseBody.body);
}
function prev() {
setPage((p) => p - 1);
}
function next() {
setPage((p) => p + 1);
}
// --- 입력은 즉시 rawKeyword에 반영, 서버요청은 디바운스 ---
function changeValue(e) {
const value = e.target.value;
setRawKeyword(value); // 즉시 DOM 갱신
}
return (
<div>
<Form className="d-flex mb-4" onSubmit={(e) => e.preventDefault()}>
<FormControl
type="search"
placeholder="Search"
className="me-2"
aria-label="Search"
value={rawKeyword} // ✅ 입력은 즉시 보이도록 rawKeyword 바인딩
onChange={changeValue}
/>
</Form>
{model.boards.map((board) => (
<BoardItem key={board.id} id={board.id} title={board.title} page={0} />
))}
<br />
<div className="d-flex justify-content-center">
<Pagination>
<Pagination.Item onClick={prev} disabled={model.isFirst}>
Prev
</Pagination.Item>
<Pagination.Item onClick={next} disabled={model.isLast}>
Next
</Pagination.Item>
</Pagination>
</div>
</div>
);
};
export default Home;변경된 화면
- 페이징 정상 적용


- 키워드를 변경했을 때 바로 적용되지 않고 일정 시간 후 검색


7. 글 상세보기, 작성, 수정, 삭제, 댓글 작성, 댓글 삭제
Detail
- 글의 정보를 받아오기 위한 useState → board 객체 저장
- 댓글을 별도로 표시하기 위한 reply 객체도 생성
- 댓글 작성 : changeValue 함수를 통해 댓글 작성
- submitReply 이벤트로 댓글 작성 통신 후 상태 갱신
- 댓글 삭제 : notifyDeleteReply 함수를 통해 삭제된 댓글 추적
- filter를 통해 삭제 버튼이 눌린 댓글의 id를 제외한 리스트만 출력
import axios from "axios";
import React, { useEffect, useState } from "react";
import { Button, Card, Form } from "react-bootstrap";
import { useSelector } from "react-redux";
import { Link, useNavigate, useParams } from "react-router-dom";
import ReplyItem from "../../components/ReplyItem";
const Detail = (props) => {
const { id } = useParams();
const jwt = useSelector((state) => state.jwt);
const navigate = useNavigate();
const [board, setBoard] = useState({
id: undefined,
title: "",
content: "",
userId: undefined,
username: "",
owner: false,
replies: [],
});
const [reply, setReply] = useState({
comment: "",
boardId: id,
});
const changeValue = (e) => {
setReply({ ...reply, comment: e.target.value });
};
async function submitReply(e) {
e.preventDefault();
let response = await axios({
method: "POST",
url: `http://localhost:8080/api/replies`,
data: reply,
headers: {
Authorization: jwt,
},
});
let responseBody = response.data.body;
// replyId, comment, boardId, userId
// id, comment, username, userId, owner
console.log("머지", responseBody);
board.replies = [responseBody, ...board.replies];
setBoard({ ...board });
}
function notifyDeleteReply(replyId) {
let newReplies = board.replies.filter((reply) => reply.id !== replyId);
board.replies = newReplies;
setBoard({ ...board });
}
useEffect(() => {
fetchDetail(id);
}, []);
async function fetchDetail(boardId) {
let response = await axios({
method: "GET",
url: `http://localhost:8080/api/boards/${boardId}`,
headers: {
Authorization: jwt,
},
});
let responseBody = response.data;
setBoard(responseBody.body);
}
async function fetchDelete(boardId) {
await axios({
method: "DELETE",
url: `http://localhost:8080/api/boards/${boardId}`,
headers: {
Authorization: jwt,
},
});
navigate("/");
}
console.log(board);
// update-form 갈때 상세보기의 상태 Board를 가져가는 것 연습해보기
return (
<div>
{board.owner ? (
<>
<Link to={`/update-form/${board.id}`} className="btn btn-warning">
수정
</Link>
<Button
className="btn btn-danger"
onClick={() => fetchDelete(board.id)}
>
삭제
</Button>
</>
) : (
<></>
)}
<br />
<br />
<h1>{board.title}</h1>
<hr />
<div>{board.content}</div>
<br />
<br />
<hr />
{/* 댓글 입력 폼 */}
<Card className="mb-4 shadow-sm border-0">
<Card.Body>
<Form onSubmit={""}>
<Form.Group className="mb-3">
<Form.Control
as="textarea"
rows={3}
placeholder="댓글을 입력하세요..."
value={reply.comment}
onChange={changeValue}
/>
</Form.Group>
<div className="d-grid gap-2 d-md-flex justify-content-md-end">
<Button variant="primary" type="submit" onClick={submitReply}>
댓글 작성
</Button>
</div>
</Form>
</Card.Body>
</Card>
{/* 댓글 목록 */}
<div className="comment-list">
{board.replies.map((reply) => (
<ReplyItem reply={reply} notifyDeleteReply={notifyDeleteReply} />
))}
</div>
</div>
);
};
export default Detail;
SaveForm
- changeValue 함수를 통해 제목, 내용 작성
- submitPost 함수를 통해 글 작성 통신 이후 홈 화면으로 이동
import axios from "axios";
import React, { useState } from "react";
import { Button, Form } from "react-bootstrap";
import { useSelector } from "react-redux";
import { useNavigate } from "react-router-dom";
const SaveForm = (props) => {
const navigate = useNavigate();
const jwt = useSelector((state) => state.jwt);
const [board, setBoard] = useState({
title: "",
content: "",
});
async function submitPost(e) {
e.preventDefault();
await axios({
method: "POST",
url: "http://localhost:8080/api/boards",
data: board,
headers: {
Authorization: jwt,
},
});
navigate("/");
}
function changeValue(e) {
setBoard({ ...board, [e.target.name]: e.target.value });
}
return (
<div>
<h1>글쓰기</h1>
<hr />
<Form>
<Form.Group>
<Form.Label>Title</Form.Label>
<Form.Control
type="text"
placeholder="Enter title"
name="title"
onChange={changeValue}
/>
</Form.Group>
<Form.Group>
<Form.Label>Content</Form.Label>
<Form.Control
as="textarea"
row={5}
name="content"
onChange={changeValue}
/>
</Form.Group>
<Button variant="primary" type="submit" onClick={submitPost}>
글등록
</Button>
</Form>
</div>
);
};
export default SaveForm;
UpdateForm
- fetchUserInfo 통신을 통해 해당 글 정보를 가져옴
- 통신한 board의 정보를 담을 state → board 객체로 저장
- changeValue 함수를 통해 글 정보 수정
- updateSubmit 함수를 통해 글 수정 통신 이후 Detail 페이지로 이동
import axios from "axios";
import React, { useEffect, useState } from "react";
import { Button, Form } from "react-bootstrap";
import { useSelector } from "react-redux";
import { useNavigate, useParams } from "react-router-dom";
const UpdateForm = (props) => {
const { id } = useParams();
const navigate = useNavigate();
const jwt = useSelector((state) => state.jwt);
const [board, setBoard] = useState({
title: "",
content: "",
});
useEffect(() => {
fetchUserInfo();
}, []);
async function updateSubmit(e) {
e.preventDefault();
try {
await axios({
method: "PUT",
url: `http://localhost:8080/api/boards/${id}`,
data: board,
headers: {
Authorization: jwt,
},
});
navigate(`/board/${id}`);
} catch (error) {
alert(error.response.data.msg);
}
}
const changeValue = (e) => {
setBoard({
...board,
[e.target.name]: e.target.value,
});
};
async function fetchUserInfo() {
let response = await axios({
method: "GET",
url: `http://localhost:8080/api/boards/${id}`,
headers: {
Authorization: jwt,
},
});
let responseBody = response.data;
setBoard({
title: responseBody.body.title,
content: responseBody.body.content,
});
}
console.log(board);
return (
<div>
<h1>글수정</h1>
<hr />
<Form>
<Form.Group>
<Form.Label>Title</Form.Label>
<Form.Control
value={board.title}
type="text"
placeholder="Enter title"
name="title"
onChange={changeValue}
/>
</Form.Group>
<Form.Group>
<Form.Label>Content</Form.Label>
<Form.Control
as="textarea"
row={5}
value={board.content}
name="content"
onChange={changeValue}
/>
</Form.Group>
<Button variant="primary" type="submit" onClick={updateSubmit}>
글수정
</Button>
</Form>
</div>
);
};
export default UpdateForm;
ReplyItem
- 댓글의 디자인 및 정보를 담는 ReplyItem
- 삭제 버튼을 눌러 해당 댓글 삭제 통신 가능
- 부모 페이지에서 댓글을 안 보이게 처리해야 하므로 함수를 dribble해서 가져와야 함
- 통신을 진행한 이후 notifyDeleteReply 호출 → 댓글 리스트 갱신
import axios from "axios";
import React from "react";
import { Button, Card } from "react-bootstrap";
import { useSelector } from "react-redux";
export default function ReplyItem(props) {
const { reply, notifyDeleteReply } = props;
const jwt = useSelector((state) => state.jwt);
async function deleteReply(replyId) {
await axios({
method: "DELETE",
url: `http://localhost:8080/api/replies/${replyId}`,
headers: {
Authorization: jwt,
},
});
notifyDeleteReply(replyId);
}
return (
<Card className="mb-3 shadow-sm border-0">
<Card.Body>
<div className="d-flex justify-content-between">
<div>
<div className="d-flex align-items-center mb-2">
<div className="flex-grow-1">
<h6 className="mb-0 fw-bold">{reply.username}</h6>
</div>
</div>
<p className="mb-2">{reply.comment}</p>
</div>
{reply.owner && (
<Button variant="danger" onClick={() => deleteReply(reply.id)}>
삭제
</Button>
)}
</div>
</Card.Body>
</Card>
);
}
변경된 화면
- 미 로그인 시 : 댓글 삭제 버튼이 없음

- 로그인 후 : 글 또는 댓글의 주인이면 글이면 수정/삭제, 댓글은 삭제 버튼 확인 가능


- 글 작성 가능

- 글 수정 가능

- 댓글 작성 및 삭제 가능

Share article