해당 글은 [React] 서버리스 메모앱-4와 이어집니다.
이번 글에서는 메모 상세조회, 수정, 삭제, 보안 페이지, 배포에 대해서 작성을 하였습니다.
메모 상세조회
메모 목록을 구성했으므로 이제 사용자가 등록한 메모를 수정할 수 있는 페이지를 구성해보자.
우선 src/Routes.tsx 파일에서 /notes/new 경로 아래에 다음 경로를 추가해주자.
import Notes from "./containers/Notes"; // 상단에 추가
<Route exact path="/notes/:id">
<Notes />
</Route>
여기서 URL에 메모 ID를 담아서 보낼 계획이기 계획이다. (React Router URL Parameter)
/notes/:id 라우터에 일치하는 경로를 노트 상세 페이지로 보낼 예정이다. 여기서 주의할 점이 /notes/new 경로도 /notes/:id 에 포함되기 때문에 반드시 /notes/new 항목 밑에 경로를 설정해야한다.
메모 조회 페이지
src/containers/Notes.tsx 파일을 생성하고 아래처럼 코드를 구성하자.
import React, { useRef, useState, useEffect } from "react";
import { useParams, useHistory } from "react-router-dom";
import { API, Storage } from "aws-amplify";
import { onError } from "../lib/errorLib";
export default function Notes() {
const file = useRef(null);
const { id } = useParams();
const history = useHistory();
const [note, setNote] = useState(null);
const [content, setContent] = useState("");
useEffect(() => {
function loadNote() {
return API.get("notes", `/notes/${id}`, {});
}
async function onLoad() {
try {
const note = await loadNote();
const { content, attachment } = note;
if (attachment) {
note.attachmentURL = await Storage.vault.get(attachment);
}
setContent(content);
setNote(note);
} catch (e) {
onError(e);
}
}
onLoad();
}, [id]);
return (
<div className="Notes"></div>
);
}
위에 코드를 살펴보자.
- useEffect Hooks를 사용하여 페이지가 렌더링되면 메모를 로드한 후에 상태값에 저장한다.
React Router와 함께 제공되는 useParams Hooks 를 사용하여 URL에서 메모ID 를 얻는다. (/notes/:id) - 첨부 파일이 있는 경우 키를 사용하여 S3에 업로드한 파일에 대한 보안 링크를 획득하고 메모 객체를 저장한다.
- note 와 함께 상태에 객체 데이터로 존재하는 이유는 사용자가 메모를 편집할 때 이것을 사용할 예정이다.
메모 양식 렌더링
src/containers/Notes.tsx 파일에서 코드를 아래처럼 수정하자.
import React, { useRef, useState, useEffect } from 'react';
import { useParams, useHistory } from 'react-router-dom';
import { API, Storage } from 'aws-amplify';
import { onError } from '../lib/errorLib';
import Form from 'react-bootstrap/Form';
import LoaderButton from '../components/LoaderButton';
import config from '../config';
import './Notes.css';
type NoteParams = {
id: string;
}
export default function Notes() {
const file = useRef<any>(null);
const { id } = useParams<NoteParams>();
const history = useHistory();
const [note, setNote] = useState<any>(null);
const [content, setContent] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [isDeleting, setIsDeleting] = useState(false);
useEffect(() => {
function loadNote() {
return API.get('notes', `/notes/${id}`, {});
}
async function onLoad() {
try {
const note = await loadNote();
const { content, attachment } = note;
if (attachment) {
note.attachmentURL = await Storage.vault.get(attachment);
}
setContent(content);
setNote(note);
} catch (e) {
onError(e);
}
}
onLoad();
}, [id]);
function validateForm() {
return content.length > 0;
}
function formatFilename(str: string) {
return str.replace(/^\w+-/, '');
}
function handleFileChange(event: any) {
file.current = event.target.files[0];
}
async function handleSubmit(event: React.MouseEvent<HTMLFormElement>) {
let attachment;
event.preventDefault();
if (file.current && file.current.size > config.MAX_ATTACHMENT_SIZE) {
alert(
`Please pick a file smaller than ${
config.MAX_ATTACHMENT_SIZE / 1000000
} MB.`
);
return;
}
setIsLoading(true);
}
async function handleDelete(event: React.MouseEvent<HTMLButtonElement>) {
event.preventDefault();
const confirmed = window.confirm(
'Are you sure you want to delete this note?'
);
if (!confirmed) {
return;
}
setIsDeleting(true);
}
return (
<div className="Notes">
{note && (
<Form onSubmit={handleSubmit}>
<Form.Group controlId="content">
<Form.Control
as="textarea"
value={content}
onChange={(e) => setContent(e.target.value)}
/>
</Form.Group>
<Form.Group controlId="file">
<Form.Label>첨부파일</Form.Label>
{note.attachment && (
<p>
<a
target="_blank"
rel="noopener noreferrer"
href={note.attachmentURL}
>
{formatFilename(note.attachment)}
</a>
</p>
)}
<Form.Control onChange={handleFileChange} type="file" />
</Form.Group>
<LoaderButton
block
size="lg"
type="submit"
isLoading={isLoading}
disabled={!validateForm()}
className={''}
>
저장
</LoaderButton>
<LoaderButton
block
size="lg"
variant="danger"
onClick={handleDelete}
isLoading={isDeleting}
className={''}
disabled={false}
>
삭제
</LoaderButton>
</Form>
)}
</div>
);
}
src/containers/Notes.css 파일을 추가하여 스타일을 구성해주자.
.Notes form textarea {
height: 300px;
font-size: 1.5rem;
}
이제 메모 리스트페이지에서 클릭시 아래처럼 표시되어야 한다.
메모 변경 사항 저장
기존 메모를 불러올 수 있으므로 해당 메모를 수정하여 저장할 수 있게 구성해보자.
src/containers/Notes.tsx 파일에서 handleSubmit() 함수를 아래처럼 수정하자.
import { s3Upload } from "../lib/awsLib"; // 상단에 추가
// 저장 동작 추가
function saveNote(note: any) {
return API.put("notes", `/notes/${id}`, {
body: note
});
}
// 아래처럼 수정
async function handleSubmit(event: any) {
let attachment;
event.preventDefault();
if (file.current && file.current.size > config.MAX_ATTACHMENT_SIZE) {
alert(
`Please pick a file smaller than ${
config.MAX_ATTACHMENT_SIZE / 1000000
} MB.`
);
return;
}
setIsLoading(true);
try {
if (file.current) {
attachment = await s3Upload(file.current);
}
await saveNote({
content,
attachment: attachment || note.attachment
});
history.push("/");
} catch (e) {
onError(e);
setIsLoading(false);
}
}
수정한 코드를 순서대로 살펴보자
- 업로드할 파일이 있으면 업로드를 s3Upload를 호출하고 S3에서 가져온 키를 저장한다.
파일이 없다면 note.attachment 상태값으로 저장한다. - PUT API를 요청하여 메모 정보를 담아 전송한다.
- 마지막으로 성공 시 사용자를 홈페이지로 리다이렉션한다.
Q. 이전에 저장된 첨부파일들은 어떻게 처리할까?
사실 새 첨부 파일을 업로드하여도 이전 첨부파일을 삭제되지 않는다.
AWS Amplfiy Stroage 왼쪽 링크를 참고하면 파일을 삭제할 수 있는 API를 추가할 수 있다.
이런 세부적인 구현사항을 프로젝트의 퀄리티를 높여주니 구현할 수 있으면 진행해보도록 하자.
메모 삭제
이제 마지막으로 사용자가 메모를 삭제할 수 있도록 구성해보자.
src/containers/Notes.tsx 파일에서 handleDelete() 함수를 아래처럼 수정해주자.
// 새로 구성
function deleteNote() {
return API.del("notes", `/notes/${id}`, {});
}
// 아래처럼 수정
async function handleDelete(event: React.MouseEvent<HTMLButtonElement>) {
event.preventDefault();
const confirmed = window.confirm(
"정말 이 메모를 삭제하시겠습니까?"
);
if (!confirmed) {
return;
}
setIsDeleting(true);
try {
await deleteNote();
history.push("/");
} catch (e) {
onError(e);
setIsDeleting(false);
}
}
이제 삭제버튼을 누르면 실제로 메모가 삭제된다. 하지만 이전처럼 첨부되어있던 파일을 삭제되지 않으니 직접 구성해보는 것을 추천한다.
보안 페이지 설정
사용자가 로그인하지 않은 경우 접근할 수 없는 페이지들이 있기때문에 보안관련 설정이 필요하다.
위의 문제를 해결하기 위해 여러방법이 존재하지만, 가장 간단한 처리방법으로 컨테이너의 조건을 확인하고 리다이렉션 하는 방법을 사용해보자.
src/components/AuthenticatedRoute.tsx 파일을 생성하고 아래처럼 코드를 구성하자.
import React from "react";
import { Route, Redirect, useLocation } from "react-router-dom";
import { useAppContext, Authentication } from "../lib/contextLib";
export default function AuthenticatedRoute({ children, ...rest }: any) {
const { pathname, search } = useLocation();
const { isAuthenticated } = useAppContext() as Authentication;
return (
<Route {...rest}>
{isAuthenticated ? (
children
) : (
<Redirect to={
`/login?redirect=${pathname}${search}`
} />
)}
</Route>
);
}
위 컴포넌트는 인증된 유저에게 필요한 페이지로 이동시켜주기 위해 구성한 것으로 간단하게 살펴보면..
- Props를 통해 children 하위 구성 요소를 받아서 렌더링
예) NewNote, Notes 컴포넌트 - AuthenticatedRoute 컴포넌트는 React Router Route 구성 요소를 반환
- useAppContext Hooks를 사용 하여 사용자가 인증되었는지 확인
- 사용자가 인증되었다면 children구성 요소를 렌더링하고, 사용자가 인증되지 않은 경우 사용자를 로그인 페이지로 리다이렉션
src/components/UnauthenticatedRoute.tsx 파일을 추가하고 아래처럼 코드를 구성하자
import React, { cloneElement } from "react";
import { Route, Redirect } from "react-router-dom";
import { useAppContext, Authentication } from "../lib/contextLib";
export default function UnauthenticatedRoute(props: any) {
const { children, ...rest } = props;
const { isAuthenticated } = useAppContext() as Authentication;
return (
<Route {...rest}>
{!isAuthenticated ? (
cloneElement(children, props)
) : (
<Redirect to="/" />
)}
</Route>
);
}
위 컴포넌트는 인증이 필요없는 페이지를 이동시켜주기 위해 작성하였다.
인증, 비인증 경로 사용
src/Routes.tsx 파일을 아래처럼 수정해주자.
import React from 'react';
import { Route, Switch } from 'react-router-dom';
import Home from './containers/Home';
import Login from './containers/Login';
import Signup from './containers/Signup';
import NewNote from './containers/NewNote';
import Notes from './containers/Notes';
import NotFound from './containers/NotFound';
import AuthenticatedRoute from './components/AuthenticatedRoute';
import UnauthenticatedRoute from './components/UnauthenticatedRoute';
export default function Routes() {
return (
<Switch>
<Route exact path="/">
<Home />
</Route>
<UnauthenticatedRoute exact path="/login">
<Login />
</UnauthenticatedRoute>
<UnauthenticatedRoute exact path="/signup">
<Signup />
</UnauthenticatedRoute>
<AuthenticatedRoute exact path="/notes/new">
<NewNote />
</AuthenticatedRoute>
<AuthenticatedRoute exact path="/notes/:id">
<Notes />
</AuthenticatedRoute>
<Route>
<NotFound />
</Route>
</Switch>
);
}
이제 로그인하지 않은 상태에서 노트페이지 경로를 적으면 로그인 페이지로 이동된다.
로그인 시 리다이렉션
로그인 후 다시 리다이렉션 설정을 위해 몇가지 작업이 필요하다.
src/components/UnauthenticatedRoute.tsx 파일을 아래처럼 수정해주자.
import React, { cloneElement } from 'react';
import { Route, Redirect } from 'react-router-dom';
import { useAppContext, Authentication } from '../lib/contextLib';
function querystring(name: string, url = window.location.href) {
const parsedName = name.replace(/[[]]/g, '\\$&');
const regex = new RegExp(`[?&]${parsedName}(=([^&#]*)|&|#|$)`, 'i');
const results = regex.exec(url);
if (!results || !results[2]) {
return false;
}
return decodeURIComponent(results[2].replace(/\+/g, ' '));
}
export default function UnauthenticatedRoute(props: any) {
const { children, ...rest } = props;
const { isAuthenticated } = useAppContext() as Authentication;
const redirect = querystring('redirect');
return (
<Route {...rest}>
{!isAuthenticated ? (
cloneElement(children, props)
) : (
<Redirect to={redirect ? redirect : '/'} />
)}
</Route>
);
}
마지막으로 src/containers/Login.tsx 파일에서 handleSubmit() 함수를 아래처럼 수정해주자.
const history = useHistory(); // 삭제해주자!!
async function handleSubmit(event: any) {
event.preventDefault();
setIsLoading(true);
try {
await Auth.signIn(fields.email, fields.password);
userHasAuthenticated(true);
} catch (e: unknown) {
onError(e);
setIsLoading(false);
}
}
이제 마지막으로 Github에 작성한 소스를 올려주자
[React] 서버리스 메모앱-5
- 메모 상세조회
- 메모 수정
- 메모 삭제
- 페이지 보안 설정
서버리스 메모 애플리케이션의 프로젝트 마지막으로 위와 같이 메모에 대한 상세처리, 페이지 보안 설정에 대해 진행하였다.
해당글을 잘 따라와주신 분들께 감사하며 전반적인 서버리스 웹 어플리케이션의 흐름을 이해하는데 많은 도움이 되었으면 좋겠다.
'IT > React' 카테고리의 다른 글
[React] 서버리스 메모앱-4 (0) | 2022.05.07 |
---|---|
[React] 서버리스 메모앱-3 (0) | 2022.04.24 |
[React] 서버리스 메모앱-2 (3) | 2022.04.18 |
[React] 서버리스 메모앱-1 (1) | 2022.04.17 |