일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 | 29 | 30 |
- 메소드오버로딩
- cursor문
- 정수형타입
- 자동차수리시스템
- 예외처리
- Java
- 환경설정
- 객체 비교
- 오라클
- abstract
- 한국건설관리시스템
- exception
- 대덕인재개발원
- 컬렉션프레임워크
- 컬렉션 타입
- EnhancedFor
- 사용자예외클래스생성
- 인터페이스
- 생성자오버로드
- 어윈 사용법
- 추상메서드
- NestedFor
- 예외미루기
- GRANT VIEW
- 참조형변수
- 자바
- 집합_SET
- 제네릭
- oracle
- 다형성
- Today
- Total
거니의 velog
(21) 프론트엔드 프로젝트 : 수정/삭제 기능 구현 및 마무리 3 본문
2. 포스트 삭제
* 마지막으로 구현할 프로젝트의 기능은 포스트 삭제이다. 이에 대한 작업을 마치고 나서 프로젝트를 마무리할 것이다.
* 삭제 버튼을 누를 때 포스트를 바로 삭제하는 것이 아니라, 사용자의 확인을 한 번 더 요청하고 나서 삭제하려고 한다. 이렇게 하는 이유는 사용자가 실수로 삭제하는 것을 방지하기 위해서이다.
* 사용자에게 한 번 더 확인을 요청하기 위해 모달 컴포넌트를 만들 것이다. 모달(modal)이란 페이지에 나타난 내용 위에 새 레이어로 어떠한 창을 보여 주는 것을 말한다.
* 이 프로젝트에서는 모달 컴포넌트를 포스트 읽기 페이지에서만 사용하지만, 컴포넌트의 재사용성을 고려하여 common 디렉터리에 만들어 볼 것이다.
* AskModal 이라는 컴포넌트를 다음과 같이 만들어 보자.
[components/common/AskModal.js]
import React from 'react';
import styled from 'styled-components';
import Button from './Button';
const Fullscreen = styled.div`
position: fixed;
z-index: 30;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.25);
display: flex;
justify-content: center;
align-items: center;
`;
const AskModalBlock = styled.div`
width: 320px;
background: white;
padding: 1.5rem;
border-radius: 4px;
box-shadow: 0px 0px 8px rgba(0, 0, 0, 0.125);
h2 {
margin-top: 0;
margin-bottom: 1rem;
}
p {
margin-bottom: 3rem;
}
.buttons {
display: flex;
justify-content: flex-end;
}
`;
const StyledButton = styled(Button)`
height: 2rem;
& + & {
margin-left: 0.75rem;
}
`;
const AskModal = ({
visible,
title,
description,
confirmText = '확인',
cancelText = '취소',
onConfirm,
onCancel,
}) => {
if (!visible) return null;
return (
<Fullscreen>
<AskModalBlock>
<h2>{title}</h2>
<p>{description}</p>
<div className="buttons">
<StyledButton onClick={onCancel}>{cancelText}</StyledButton>
<StyledButton cyan onClick={onConfirm}>
{confirmText}
</StyledButton>
</div>
</AskModalBlock>
</Fullscreen>
);
};
export default AskModal;
* 방금 만든 AskModal을 기반으로 post 디렉터리에 AskRemoveModal 이라는 컴포넌트를 만들어 보자.
[components/post/AskRemoveModal.js]
import React from 'react';
import AskModal from '../common/AskModal';
const AskRemoveModal = ({ visible, onConfirm, onCancel }) => {
return (
<AskModal
visible={visible}
title="포스트 삭제"
description="포스트를 정말 삭제하시겠습니까?"
confirmText="삭제"
onConfirm={onConfirm}
onCancel={onCancel}
/>
);
};
export default AskRemoveModal;
* AskRemoveModal 컴포넌트를 굳이 이렇게 별도의 파일로 분리하여 만들어 줄 필요는 없다. 그냥 모달을 사용하는 곳에서 AskModal을 직접 렌더링해도 상관없다. 다만, 모달별로 이렇게 파일을 만들어 주면 나중에 모달의 개수가 많아졌을 때 관리하기가 매우 편해진다.
* 컴포넌트를 다 만들었으면 PostActionButtons 내부에서 사용해 보자.
[components/post/PostActionButtons.js]
import React, { useState } from 'react';
import styled from 'styled-components';
import palette from '../../lib/styles/palette';
import AskRemoveModal from './AskRemoveModal';
(...)
const PostActionButtons = ({ onEdit, onRemove }) => {
const [modal, setModal] = useState(false);
const onRemoveClick = () => {
setModal(true);
};
const onCancel = () => {
setModal(false);
};
const onConfirm = () => {
setModal(false);
onRemove();
};
return (
<>
<PostActionButtonsBlock>
<ActionButton onClick={onEdit}>수정</ActionButton>
<ActionButton onClick={onRemoveClick}>삭제</ActionButton>
</PostActionButtonsBlock>
<AskRemoveModal
visible={modal}
onConfirm={onConfirm}
onCancel={onCancel}
/>
</>
);
};
export default PostActionButtons;
* 이제 삭제 버튼을 눌러 보자. 모달이 잘 나타날 것이다. 취소 버튼을 눌러서 모달이 잘 사라지는 지도 확인해 보자. 모달 내부의 삭제 버튼을 누르면 오류가 발생할 것이다. 아직 onRemove를 넣어 주지 않았기 때문이다.
* onRemove를 구현하기에 앞서 lib/api/posts.js 파일에 removePost 함수를 구현해 주자.
[lib/api/posts.js]
(...)
export const removePost = (id) => client.delete(`/api/posts/${id}`);
* 이제 PostViewer 에서 onRemove 함수를 만들어 removePost를 호출하도록 구현해 볼 것이다. removePost의 경우 API를 요청한 후 따로 보여 주어야 할 결과가 없으니 리덕스 액션과 사가를 만드는 작업을 생략하고 바로 API를 사용해 볼 것이다.
[containers/post/PostViewerContainer.js]
import React, { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { withRouter } from 'react-router-dom';
import { readPost, unloadPost } from '../../modules/post';
import PostViewer from '../../components/post/PostViewer';
import PostActionButtons from '../../components/post/PostActionButtons';
import { setOriginalPost } from '../../modules/write';
import { removePost } from '../../lib/api/posts';
const PostViewerContainer = ({ match, history }) => {
(...)
const onRemove = async () => {
try {
await removePost(postId);
history.push('/'); // 홈으로 이동
} catch (e) {
console.log(e);
}
};
const ownPost = (user && user._id) === (post && post.user._id);
return (
<PostViewer
post={post}
loading={loading}
error={error}
actionButtons={
ownPost && <PostActionButtons onEdit={onEdit} onRemove={onRemove} />
}
/>
);
};
export default withRouter(PostViewerContainer);
* 컴포넌트 수정이 끝나고 나면, 다시 삭제 버튼을 눌러서 모달을 열고 삭제 버튼을 눌러 보자. 홈 화면으로 이동했는가? 삭제한 포스트가 홈 화면에서 사라졌는지도 확인해 보자.
* 이제 이 프로젝트의 주요 기능을 모두 구현했다!
3. react-helmet-async로 meta 태그 설정하기
* 현재 우리가 만든 웹 애플리케이션을 브라우저에서 열어 보면 상단에 React App이라는 제목이 나타난다.
* 구글, 네이버 같은 검색 엔진에서 웹 페이지를 수집할 때는 meta 태그를 읽는데, 이 meta 태그를 리액트 앱에서 설정하는 방법을 한번 알아 보자.
* 우선 yarn을 사용하여 클라이언트 프로젝트에 react-helmet-async 라는 라이브러리를 설치하자.
$ yarn add react-helmet-async
* 다음으로 src/index.js 파일을 열어서 HelmetProvider 컴포넌트로 App 컴포넌트를 감싸자.
(...)
import { HelmetProvider } from 'react-helmet-async';
(...)
ReactDOM.render(
<Provider store={store}>
<BrowserRouter>
<HelmetProvider>
<App />
</HelmetProvider>
</BrowserRouter>
</Provider>,
document.getElementById('root'),
);
* 그리고 나서 meta 태그를 설정하고 싶은 곳에 Helmet 컴포넌트를 사용하면 된다. App 컴포넌트를 다음과 같이 수정해 보자.
[App.js]
import React from 'react';
import { Route } from 'react-router-dom';
import PostListPage from './pages/PostListPage';
import LoginPage from './pages/LoginPage';
import RegisterPage from './pages/RegisterPage';
import WritePage from './pages/WritePage';
import PostPage from './pages/PostPage';
import { Helmet } from 'react-helmet-async';
const App = () => {
return (
<>
<Helmet>
<title>REACTERS</title>
</Helmet>
<Route component={PostListPage} path={['/@:username', '/']} exact />
<Route component={LoginPage} path="/login" />
<Route component={RegisterPage} path="/register" />
<Route component={WritePage} path="/write" />
<Route component={PostPage} path="/@:username/:postId" />
</>
);
};
export default App;
* 이제 브라우저에서 페이지 제목이 REACTERS로 바뀐 것을 볼 수 있다.
* react-helmet-async 에서는 더 깊숙한 곳에 위치한 Helmet이 우선권을 차지한다. 예를 들어 App과 WritePage에서 Helmet을 사용할 경우, WritePage는 App 내부에 들어 있기 때문에 WritePage에서 설정하는 title 값이 나타난다.
* WritePage 에서도 Helmet을 한번 사용해 보자.
[pages/WritePage.js]
import React from 'react';
import Responsive from '../components/common/Responsive';
import EditorContainer from '../containers/write/EditorContainer';
import TagBoxContainer from '../containers/write/TagBoxContainer';
import WriteActionButtonsContainer from '../containers/write/WriteActionButtonsContainer';
import { Helmet } from 'react-helmet-async';
const WritePage = () => {
return (
<Responsive>
<Helmet>
<title>글 작성하기 - REACTERS</title>
</Helmet>
<EditorContainer />
<TagBoxContainer />
<WriteActionButtonsContainer />
</Responsive>
);
};
export default WritePage;
* 코드를 저장한 후 /write 페이지로 들어가 보자.
* 제목이 새 글 작성하기로 잘 바뀌었는가?
* 그 다음에는 PostViewer 컴포넌트에서 Helmet을 사용하여 포스트의 제목이 페이지의 제목이 되도록 설정해 보자.
[components/post/PostViewer.js]
import React from 'react';
import styled from 'styled-components';
import palette from '../../lib/styles/palette';
import Responsive from '../common/Responsive';
import SubInfo from '../common/SubInfo';
import Tags from '../common/Tags';
import { Helmet } from 'react-helmet-async';
(...)
const PostViewer = ({ post, error, loading, actionButtons }) => {
(...)
return (
<PostViewerBlock>
<Helmet>
<title>{title} - REACTERS</title>
</Helmet>
<PostHead>
<h1>{title}</h1>
<SubInfo
username={user.username}
publishedDate={publishedDate}
hasMarginTop
/>
<Tags tags={tags} />
</PostHead>
{actionButtons}
<PostContent dangerouslySetInnerHTML={{ __html: body }} />
</PostViewerBlock>
);
};
export default PostViewer;
* Helmet을 적용한 후 아무 포스트나 열어 보자. 페이지 제목이 잘 바뀌는 것을 확인할 수 있다.
4. 프로젝트 마무리
* 프로젝트를 완성한 뒤에는 어떠한 작업을 해야 하는지 알아보자.
(1) 프로젝트 빌드하기
* 우선 백엔드 서버를 통해 리액트 앱을 제공할 수 있도록 빌드해 주어야 한다. 클라이언트 프로젝트 디렉터리에서 다음 명령어를 실행하자.
$ yarn build
* 작업이 끝나면 blog-frontend에 build 디렉터리가 생성된다.
(2) koa-static 으로 정적 파일 제공하기
* 서버를 통해 blog-frontend/build 디렉터리 안의 파일을 사용할 수 있도록 koa-static을 사용하여 정적 파일 제공 기능을 구현해 보자.
* 서버 프로젝트 디렉터리에서 다음 명령어를 실행하여 koa-static을 설치하자.
$ yarn add koa-static
* 이어서 main.js를 다음과 같이 수정해 보자.
require('dotenv').config();
import Koa from 'koa';
import Router from 'koa-router';
import bodyParser from 'koa-bodyparser';
import mongoose from 'mongoose';
import serve from 'koa-static';
import path from 'path';
import send from 'koa-send';
import api from './api';
import jwtMiddleware from './lib/jwtMiddleware';
// 비구조화 할당을 통하여 process.env 내부 값에 대한 레퍼런스 만들기
const { PORT, MONGO_URI } = process.env;
mongoose
.connect(MONGO_URI, { useNewUrlParser: true, useFindAndModify: false })
.then(() => {
console.log('Connected to MongoDB');
})
.catch((e) => {
console.error(e);
});
const app = new Koa();
const router = new Router();
// 라우터 설정
router.use('/api', api.routes()); // api 라우트 적용
// 라우터 적용 전에 bodyParser 적용
app.use(bodyParser());
app.use(jwtMiddleware);
// app 인스턴스에 라우터 적용
app.use(router.routes()).use(router.allowedMethods());
const buildDirectory = path.resolve(__dirname, '../../blog-frontend/build');
app.use(serve(buildDirectory));
app.use(async (ctx) => {
// Not Found 이고, 주소가 /api 로 시작하지 않는 경우
if (ctx.status === 404 && ctx.path.indexOf('/api') !== 0) {
// index.html 내용을 반환
await send(ctx, 'index.html', { root: buildDirectory });
}
});
// PORT 가 지정되어있지 않다면 4000 을 사용
const port = PORT || 4000;
app.listen(port, () => {
console.log('Listening to port %d', port);
});
* use-static을 사용하여 blog-frontend/build 디렉터리에 있는 파일들을 서버를 통해 조회할 수 있게 해 주었다. 추가로 하단에 send 라는 함수를 사용하는 미들웨어를 작성했는데, 이 미들웨어는 클라이언트 기반 라우팅이 제대로 작동하게 해 준다. HTTP 상태가 404이고 주소가 /api로 시작하지 않으면, index.html의 내용을 응답한다. 이 미들웨어를 적용하지 않으면 http://localhost:4000/write 페이지를 브라우저 주소창에 직접 입력하여 들어갈 경우, 페이지가 제대로 나타나지 않고 Not Found 가 나타나게 된다.
* 이제 브라우저 주소창에 http://localhost:4000/ 주소를 입력하여 들어가 보자. 개발 서버에서 보았던 화면에 제대로 나타나는가? 포스트 페이지를 열고 새로고침을 했을 때도 포스트 내용이 잘 나타나는지 확인해 보자.
'React_프론트엔드 프로젝트' 카테고리의 다른 글
(22) 프론트엔드 프로젝트 : 프로젝트 마무리, 그 이후 개발 방향? (0) | 2024.02.24 |
---|---|
(20) 프론트엔드 프로젝트 : 수정/삭제 기능 구현 및 마무리 2 (0) | 2024.02.23 |
(19) 프론트엔드 프로젝트 : 수정/삭제 기능 구현 및 마무리 1 (0) | 2024.02.23 |
(18) 프론트엔드 프로젝트 : 포스트 조회 기능 구현하기 5 (0) | 2024.02.23 |
(17) 프론트엔드 프로젝트 : 포스트 조회 기능 구현하기 4 (0) | 2024.02.23 |