일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | ||||||
2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 |
- C++
- react-redux
- 매일메일
- axios
- Python
- sw expert academy
- redux-saga
- 항해99
- programmers
- redux
- react-router
- SW
- 테코테코
- JavaScript
- useDispatch
- 항해플러스
- 코딩테스트합격자되기
- Algorithm
- react
- 알고리즘
- Get
- json-server
- maeil-mail
- 리액트
- createSlice
- java
- redux-toolkit
- 이코테
- 자바
- 프로그래머스
- Today
- Total
Binary Journey
[React][CRUD] 게시판 만들기 All in One (7). 새 글 쓰기, axios, post 본문
[React][CRUD] 게시판 만들기 All in One (7). 새 글 쓰기, axios, post
binaryJournalist 2021. 4. 14. 17:14
목차돌아가기:
binaryjourney.tistory.com/pages/ReactCRUD-create-board-tutorial-v2
이제까지 거의 GET방식을 이용한 조회를 해봤다 (조회수 update, 댓글 등록, 댓글삭제 제외)
이번엔 새 글 쓰기를 해보겠다.
새 글 쓰기는 지난번 Route.js 에서
경로가 "/insert" 일 때 컴포넌트는 Post 로 렌더링되도록 설정해놓았다.
새글 쓰기는 어디에서나 가능하도록 하려 한다. 그래서 Views.js 에 id=header 인 태그 부분에 이벤트함수를 다음과 같이 집어넣었다.
// Views.js
import React from 'react';
import Board from "./views/Board";
import Routes from "./routes/Routes";
import "./Views.css";
import { useHistory } from 'react-router';
function Views() {
const history = useHistory();
function onClickNewPostButton() {
history.push("/insert");
}
function onClickControlButton() {
history.push("/control");
}
return (
<div >
<div id="header" className="header">
<div >
<h3>Board CRUD</h3>
</div>
<div>
<div>
<button onClick={onClickNewPostButton}>새글</button>
</div>
</div>
</div>
<div id="sidebar" className="sidebar">
<Board />
</div>
<div id="content" className="content">
<Routes />
</div>
</div>
);
}
export default Views;
버튼을 누르면 이렇게 될 것이다.
이제 Post를 좀 그럴듯한 글쓰기 화면으로 바꾸겠다
// Posts.js
import React from 'react';
function Post() {
return (
<div>
<div>
<span>게시판: </span>
<select>
<option>선택</option>
<option></option>
<option></option>
</select>
</div>
<div>
<span>제목: </span><input></input>
</div>
<div>
<span>내용: </span><textarea></textarea>
</div>
</div>
);
}
export default Post;
(이번에는 게시판 고르는 combo box도 추가되었다.)
게시글 쓰는 부분도 useState로만 진행하려고 한다.
redux 의 useSelector 를 쓰는 부분은 게시판 combo box에만 사용할 것 같다.
나는 게시판 목록을 서버에서 다시 불러와서 쓸 게 아니라 state에 있는 boardList 다시 불러와서 쓸 계획이었는데
화면 구조 상 게시판 정보가 없어도 Header 부분 새글 버튼만 눌러도 Post로 이동이 가능하므로
Post 에 게시판이 없을 때 보여 줄 JSX를 만들어야 겠다.
나는 이렇게 바꿔주었다.
// Post.js
import React from 'react';
import { useSelector } from 'react-redux';
function Post() {
const { boardList, status, statusText } = useSelector((state) => state.boardReducer);
return (
<div>
{ status === 200 && boardList.length > 0 ?
(
<>
<div>
<span>게시판: </span>
<select>
<option>선택</option>
<option></option>
<option></option>
</select>
</div>
<div>
<span>제목: </span><input></input>
</div>
<div>
<span>내용: </span><textarea></textarea>
</div>
</>
) : status === 200 && boardList.length === 0 ?
(
<div>
게시판 등록이 필요합니다.
</div>
) : (
<div>
<div>
<span>{status}</span>
</div>
<div>
<span>{statusText}</span>
</div>
</div>
)
}
</div>
);
}
export default Post;
이제 combo box 를 바꿔줄 차례인데 나는 combo box 와 관련해서 Constants를 아예 utils 에 공통으로 쓸 수 있도록 만들었다.
/utils/events.js
export const SELECT = { id: 0, name: "선택" };
export const WHOLE = { id: 0, name: "전체" };
(소스를 다시 정리해보니 event.js 는 결국 안 썼다)
// Post.js
import React from 'react';
import { useSelector } from 'react-redux';
import { SELECT } from '../utils/events';
function Post() {
const { boardList, status, statusText } = useSelector((state) => state.boardReducer);
return (
<div>
{ status === 200 && boardList.length > 0 ?
(
<>
<div>
<span>게시판: </span>
<select name="boardId">
<option value={SELECT.id}>{SELECT.name}</option>
{
boardList.map((board) => (
<option value={board?.id}>{board?.name ?? ""}</option>
))
}
</select>
</div>
<div>
<span>제목: </span><input></input>
</div>
<div>
<span>내용: </span><textarea></textarea>
</div>
</>
) : status === 200 && boardList.length === 0 ?
(
<div>
게시판 등록이 필요합니다.
</div>
) : (
<div>
<div>
<span>{status}</span>
</div>
<div>
<span>{statusText}</span>
</div>
</div>
)
}
</div>
);
}
export default Post;
게시판 combo box에 값이 들어가있는지 확인해보자
useState로 object 값 넣을 수 있는지 해봤는데 되길래 소스를 올린다.
// Post.js
import React, { useState } from 'react';
import { useSelector } from 'react-redux';
import { SELECT } from '../utils/events';
function Post() {
const { boardList, status, statusText } = useSelector((state) => state.boardReducer);
const [ article, setArticle ] = useState({});
function onChangeArticle(e) {
setArticle({
...article,
[e.currentTarget.name]: e.currentTarget.value
});
}
return (
<div>
{ status === 200 && boardList.length > 0 ?
(
<>
<div>
<span>게시판: </span>
<select
name="boardId"
onChange={onChangeArticle}
value={article?.boardId ?? 0}
>
<option value={SELECT.id} key={SELECT.id}>{SELECT.name}</option>
{
boardList.map((board, index) => (
<option value={board?.id} key={board?.id}>{board?.name ?? ""}</option>
))
}
</select>
</div>
<div>
<span>제목: </span>
<input
name="title"
onChange={onChangeArticle}
value={article?.title ?? ""}
/>
</div>
<div>
<span>내용: </span>
<textarea
name="content"
onChange={onChangeArticle}
value={article?.content ?? ""}
/>
</div>
</>
) : status === 200 && boardList.length === 0 ?
(
<div>
게시판 등록이 필요합니다.
</div>
) : (
<div>
<div>
<span>{status}</span>
</div>
<div>
<span>{statusText}</span>
</div>
</div>
)
}
</div>
);
}
export default Post;
input에 값 집어넣기가 되니 이제 서버에 보내보자
게시판 태그들을 감쌌던 <></> 안에 등록 버튼을 만들어준다.
// Post.js
<>
<div>
<span>게시판: </span>
<select
name="boardId"
onChange={onChangeArticle}
value={article?.boardId ?? 0}
>
<option value={SELECT.id} key={SELECT.id}>{SELECT.name}</option>
{
boardList.map((board, index) => (
<option value={board?.id} key={board?.id}>{board?.name ?? ""}</option>
))
}
</select>
</div>
<div>
<span>제목: </span>
<input
name="title"
onChange={onChangeArticle}
value={article?.title ?? ""}
/>
</div>
<div>
<span>내용: </span>
<textarea
name="content"
onChange={onChangeArticle}
value={article?.content ?? ""}
/>
</div>
<button>등록</button>
</>
articleSlice 와 articleSaga 에 등록과 관련된 액션과 함수들을 만든다.
// articleSlice.js
postArticle: (state, action) => {},
postArticleSuccess: (state, action) => {},
postArticleFail: (state, action) => {
state.status = action.payload?.status ?? 500;
state.statusText = action.payload?.statusText ?? "Network Error";
},
// articleSaga.js
function apiPostArticle(requestBody) {
return axios.post(`articles/`, requestBody);
}
function* asyncPostArticle(action) {
try {
const response = yield call(apiPostArticle, {
...action.payload,
id: 0,
views: 0,
insertDate: Date.now(),
updateDate: Date.now()
});
if (response?.status === 201) {
yield put(articleActions.postArticleSuccess());
history.push(`/article/${response?.data?.id ?? 0}`);
} else {
yield put(articleActions.postArticleFail(response));
yield alert(`등록실패 \n Error: ${response.status}, ${response.statusText}`);
}
} catch(e) {
console.error(e);
yield put(articleActions.postArticleFail(e?.response));
yield alert(`등록실패 \n Error: ${e?.response?.status}, ${e?.response?.statusText}`);
}
}
function* watchPostArticle() {
while(true) {
const action = yield take(articleActions.postArticle);
yield call(asyncPostArticle, action);
}
}
export default function* articleSaga()
{
yield all([fork(watchGetArticleList), fork(watchGetArticle), fork(watchUpdateArticleViews), fork(watchPostArticle)]);
}
등록 실패시 새로고침하여 내용을 다 날려버리는 것은 선택 사항이다.
필요하다면 history.go(0); 쓰면 된다.
그리고 history와 관련하여
utils 에 있는 history를 import하는 게 싫다면
import { all, call, retry, fork, put, take, select, getContext } from 'redux-saga/effects';
function* asyncPostArticle(action) {
try {
const history = yield getContext("history"); // react-router-dom 의 BrowserRouter 에서는 안 됨
const response = yield call(apiPostArticle, {
...action.payload,
id: 0,
views: 0,
insertDate: Date.now(),
updateDate: Date.now()
});
if (response?.status === 201) {
yield put(articleActions.postArticleSuccess());
history.push(`/article/${response?.data?.id ?? 0}`);
} else {
yield put(articleActions.postArticleFail(response));
yield alert(`등록실패 \n Error: ${response.status}, ${response.statusText}`);
}
} catch(e) {
console.error(e);
yield put(articleActions.postArticleFail(e?.response));
yield alert(`등록실패 \n Error: ${e?.response?.status}, ${e?.response?.statusText}`);
}
}
store 의 sagaMiddleware 에서 넣은 context: { history: history } 를 getContext("history") 로 꺼내와서 쓰면 된다.
초반에 말했다시피 Router 가 BrowserRouter 가 아니면 된다.
views/Post.js 는 다음과 같다
// Post.js
import React, { useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { SELECT } from '../utils/events';
import { articleActions } from '../slices/articleSlice';
function Post() {
const { boardList, status, statusText } = useSelector((state) => state.boardReducer);
const [ article, setArticle ] = useState({});
const dispatch = useDispatch();
function onChangeArticle(e) {
setArticle({
...article,
[e.currentTarget.name]: e.currentTarget.value
});
}
function onClickSubmitButton() {
if (article?.boardId > 0 && article?.title)
{
dispatch(articleActions.postArticle(article));
} else {
alert("게시판과 제목은 필수값입니다.");
}
}
return (
<div>
{ status === 200 && boardList.length > 0 ?
(
<>
<div>
<span>게시판: </span>
<select
name="boardId"
onChange={onChangeArticle}
value={article?.boardId ?? 0}
>
<option value={SELECT.id} key={SELECT.id}>{SELECT.name}</option>
{
boardList.map((board, index) => (
<option value={board?.id} key={board?.id}>{board?.name ?? ""}</option>
))
}
</select>
</div>
<div>
<span>제목: </span>
<input
name="title"
onChange={onChangeArticle}
value={article?.title ?? ""}
/>
</div>
<div>
<span>내용: </span>
<textarea
name="content"
onChange={onChangeArticle}
value={article?.content ?? ""}
/>
</div>
<button onClick={onClickSubmitButton}>등록</button>
</>
) : status === 200 && boardList.length === 0 ?
(
<div>
게시판 등록이 필요합니다.
</div>
) : (
<div>
<div>
<span>{status}</span>
</div>
<div>
<span>{statusText}</span>
</div>
</div>
)
}
</div>
);
}
export default Post;
내용 잘 나오는지 확인해보자. url에도 아이디가 잘 들어가는지 보자
게시판 메뉴 눌렀을 때 목록으로도 잘 나오는지 보자
목차돌아가기:
binaryjourney.tistory.com/pages/ReactCRUD-create-board-tutorial-v2