※ React 를 사용해 만든 메모장 SPA 어플리케이션
인프런의 프로젝트로 배우는 React.js 강의를 듣고 응용하여 만든 프로젝트입니다.
⬇️
✅ 목표
- React, Hook, SPA, ES6 이해 및 학습
⚒️ Dependency
- axios
- json-server
- prop-types
- react-router-dom
🔫 요구사항
- 메모 등록, 조회, 수정, 삭제
- 조회 시 로딩 중 Spinner 보여주기
- NavBar 페이지 나누기
▶ 라우팅
App.js
<Route path="/" exact>
<HomePage />
</Route>,
<Route path="/blogs" exact>
<ListPage />
</Route>,
<Route path="/blogs/create" exact>
<CreatePage />
</Route>,
<Route path="/blogs/edit" exact>
<EditPage />
</Route>
이런 방법도 있지만
아래와 같이 작성하면 컴포넌트가 많아질 때 코드의 재사용성을 높일 수 있다.
routes.js
const routes =[
{
path:'/',
component: HomePage
},
{
path:'/memos',
component: ListPage
},
{
path:'/memos/create',
component: CreatePage
},
{
path:'/memos/:id/edit',
component: EditPage
},
{
path: '/memos/:id',
component:ShowPage
},
];
export default routes;
App.js
function App() {
return (
<Router>
<NavBar />
<div className="container mt-3">
<Switch>
{routes.map((route)=> {
return <Route
key ={route.path}
exact path ={route.path}
component ={route.component}/>
})}
</Switch>
</div>
</Router>
);
}
export default App;
컴포넌트가 추가되면 routes.js 배열에 추가만해주면 된다.
그리고 App.js 에서 map 함수에서 호출하여 <Route/> 컴포넌트의 value로 넣어주면 깔끔해진다.
사용된 컴포넌트는 다음과 같은 역할을 한다
▷ BrowserRouter (as Router)
웹 어플리케이션이 History API 를 사용해 페이지를 새로고침 없이 주소를 변경할 수 있게 해준다.
▷ Switch
Route 로 생성된 자식 컴포넌트중 매칭되는 첫번째 Route 를 렌더링 해준다.
Switch 를 사용하지 않으면 현재위치와 일치하는 컴포넌트들이 모두 렌더링되어 화면에 보여진다.
▷ Route
지정된 경로에 따라 다른 컴포넌트를 보여준다.
▶ NavBar
가장 먼저 페이지 상단에 고정되어 있는 NavBar 컴포넌트를 만들었다
import { Link, NavLink } from "react-router-dom";
const NavBar = () => {
return (
<nav className="navbar navbar navbar-dark bg-dark">
<div className="container">
<Link className="navbar-brand" to="/">
Home
</Link>
<ul className="navbar-nav">
<li className="nav-item">
<NavLink
activeClassName="active"
className="nav-link"
aria-current="page"
to="/memos"
>
MEMO
</NavLink>
</li>
</ul>
</div>
</nav>
);
};
export default NavBar;
React-router-dom@5 의 컴포넌트인 NavLink 는 Link 컴포넌트의 스페셜 기능으로서,
특정 링크에 스타일을 추가할 수 있다. 여기서 NavLink 는 'acitveClassName' 속성에 active 를 줘서
해당 링크로 접속하게 되면 밝게 빛나게 해주는 역할을 하고있다.
▶ 메모장 목록
db 에 저장된 메모장 목록을 보여주는 페이지이다.
코드가 길어서 역할별로 소개해보기로 하겠다.
▷ DB에 메모장 List 데이터 요청하기
useEffect 를 통해 컴포넌트가 마운트 될 때 getPost() 함수를 수행하여 db로부터 메모장 목록을 받아온다.
const getPosts = () => {
axios.get("http://localhost:3001/memos").then((res) => {
setPosts(res.data); //받아온 데이터를 state 안에 넣어주기
setLoading(false); //로딩이 완료되면 false 로 변경
});
};
useEffect(() => {
getPosts(); //useEffect 는 getPost 가 한번만 실행되도록 도와줌
}, []);
getPost() 함수는 db에 메모 목록을 요청하고 응답을 받아오는 메서드이다.
useEffect 는 컴포넌트가 마운트(처음 나타났을 때) 되었을 때 특정 작업을 수행하는 React 의 Hook 이다.
위 코드와 같이 두번째 인자로 빈 배을 넣으면 마운트 되었을 때만 작업을 수행하고,
값을 넣을 경우 해당 값이 update 될 때마다 작업을 수행한다.
그리고 loading useState 값을 true 로 set 한다.
loading state 가 어떤 역할을 하는지는 바로 아래에서 설명하겠다.
▷ LoadingSpinner 컴포넌트
const renderBlogList = () => {
if (loading) {
return <LoadingSpinner />;
}else{
return <메모리스트/>
}
...
getPost() 함수에서 서버로부터 응답을 받아온 후 loading=false 가 되면
가져온 데이터를 뿌려서 리스트를 보여주고, 그렇지 않으면 LoadingSpinner 컴포넌트를 보여준다.
setLoading(false) 상태에서 사용자가 볼 화면은 아래와 같다.
LoadingSpinner 컴포넌트는 Bootstrap 에서 템플릿을 가져다 쓰고 export 했다.
▷ 메모 삭제하기
Delete 버튼을 누르면 메모를 삭제할 수 있다. deleteMemo 는 버튼이 클릭되면 실행되는 함수이다.
const deleteMemo = (e, id) => {
axios.delete(`http://localhost:3001/memos/${id}`).then(() => {
});
};
DB에 데이터를 삭제요청하는 코드만 넣었더니 두가지 문제가 발생한다.
1. 이벤트 버블링
- 삭제 버튼을 누르면
- 삭제 버튼을 누르면 실행되는 동작과
- 삭제버튼의 부모 엘리먼트인 <Card/> 컴포넌트를 눌렀을 때 실행되는 동작이
- 함께 실행된다.
const deleteMemo = (e, id) => {
e.stopPropagation();
axios.delete(`http://localhost:3001/memos/${id}`).then(() => {
});
};
e.stopPropagation 를 통해 이벤트가 상위 엘리먼트에 전달되지 않게 막아 준다. 해결 !
2. 삭제해도 화면에 그대로 남아있음
- 삭제 버튼을 누르면
- DB에서만 삭제가 되고
- 화면에는 삭제된 메모가 그대로 남아있음
- 새로고침을 해야만 메모가 사라짐
리스트를 자동 업데이트하여 삭제된 메모를 없애주는 작업을 해보자
const deleteMemo = (e, id) => {
e.stopPropagation();
axios.delete(`http://localhost:3001/memos/${id}`).then(() => {
setPosts((prevPosts) => {
return prevPosts.filter((post) => {
return post.id !== id;
});
});
});
};
filter 함수는 남아있는 리스트에서 삭제된 메모의 id 와 같지 않을 경우에만 남겨둔다.
따라서 삭제된 메모는 filter 에 의해 걸러진다. 해결 !
▷ 목록 보여주기
ListPage.js
const renderBlogList = () => {
...
if (posts.length === 0) {
return <div>No blog posts found</div>;
}
return posts.map((post) => {
return (
<Card
key={post.id}
title={post.title}
onClick={() => history.push(`/memos/${post.id}`)}
>
<div>
<button
className="btn btn-danger btn-sm"
//onClick= {()=>console.log('delete')}
onClick={(e) => deleteBlog(e, post.id)}
>
Delete
</button>
</div>
</Card>
);
});
};
만약 등록된 메모가 없다면 문구를 보여주고, 그렇지 않을 땐 map 함수를 실행해서 데이터를 뿌려준다.
해당(ListPage.js) 컴포넌트에서는 메모장 리스트 데이터가 확인 가능하지만 자식 컴포넌트인 Card 는 알 수 없다.
따라서 Card 컴포넌트로 데이터를 props 로 넘겨주었다.
Card.js
import PropTypes from 'prop-types'
const Card = ({ title, children, onClick }) => {
return (
<div
className="card mb-3 cursor-pointer"
onClick = {onClick}>
<div className="card-body">
<div className="d-flex justify-content-between">
<div>{title}</div>
{children && <div>{children}</div>}
</div>
</div>
</div>
);
};
...
자식 컴포넌트에서 props 를 전달 받을 때
const 자식컴포넌트=(props)=>{}
위와 같이 전달할 수 있는데, 이럴땐 props.title 로 전달 받은 props 를 꺼내 쓸 수 있다.
const Card = ({ title, children, onClick }) =>{}
이렇게 props 를 구조분해하여 전달할 수도 있다. 바로 title 에 접근이 가능하다.
React 에 내장된 타입검사 기능인 prop-types 라이브러리를 사용해 타입을 검증할 수 있다.
import PropTypes from 'prop-types'
...
Card.propTypes = {
title : PropTypes.string.isRequired,
children : PropTypes.element,
onClick : PropTypes.func, //Card 컴포넌트에서만 onClick 이벤트 가능하도록
};
//props 의 default 값을 설정
Card.defaultProps = {
children : null,
onClick: ()=> {},
};
export default Card;
다음과 같이 타입을 지정할 수 있다. 지정된 타입이 아닌값이 할당되면 console 창에 warning 이 발생한다.
또한`isRequired`와 연결하여 prop가 제공되지 않았을 때 경고가 보이도록 할 수 있다.
다시 ListPage.js 로 돌아와서, ListPage 컴포넌트는 아래 html 을 리턴한다
return (
<div>
<div className="d-flex justify-content-between">
<h1>Blogs</h1>
<Link to="/memos/create" className="btn btn-success mb-2">
Create New
</Link>
</div>
{renderBlogList()}
</div>
);
Create New 버튼을 누르면 메모를 생성하는 페이지로 넘어간다.
▶ 메모 생성, 수정
생성 및 수정은 한개의 컴포넌트에 넣었다.
▷ 서버에 메모 상세 데이터 요청
const Form = ({editing}) => {
const {id} = useParams();
...
useEffect(()=>{
if(editing){
axios.get(`http://localhost:3001/memos/${id}`).then(res => {
setTitle(res.data.title);
setOriginalTitle(res.data.title);
setBody(res.data.body);
setOriginalBody(res.data.body);
})
}
}, [id,editing]);
Form 컴포넌트는 EditPage 부모컴포넌트로부터 props(editing) 을 받는다.
만약 editing 이 true 라면 useParam 으로 받은 id를 통해 해당 메모의 데이터를 요청하고
이를 4개의 state 에 set 한다.
▷ 수정 버튼 활성화
const isEdited = () =>{
return title !== originalTitle || body !== originalBody
}
4개의 state 는 수정된 값이 있을 때 버튼을 활성화 시켜주는 역할을 한다.
그러니까 title 과 body 둘중 하나라도 변경이 되어야만 아래와 같이 Edit 버튼이 활성화되는 것이다.
이 버튼을 누르면 onSubmit() 이 실행된다. onsubmit 은 서버에 데이터 등록 또는 수정을 요청하는 함수이다
const onSubmit = () => {
if(editing) {
axios.patch(`http://localhost:3001/memos/${id}`, {
title: title,
body: body,
}).then((res)=>{
history.push(`/memos/${id}`)
})
}else{
axios
.post("http://localhost:3001/memos", {
title: title,
body: body,
createdAt: Date.now(), // Date.now() : js 에서 시간을 가져오기
})
.then(() => {
history.push("/memos");
});
}
};
▷ 취소 버튼
const goBack = () => {
if(editing){
history.push(`/memos/${id}`);
}else{
history.push(`/memos`);
}
};
cancle 버튼을 눌렀을 때 발생하는 goback() 은 수정페이지면 상세페이지로,
생성 페이지면 메모 목록 페이지로 이동하게 한다.
Form.js 가 리턴하는 html 은 특별히 설명할 것이 없어서 첨부하지 않겠다.
코드는 아래 github 에서 확인 가능하다.
▶ 메모 상세 보기
▷ 서버에 메모 상세 정보 요청
const { id } = useParams();
const getPost = (id) => {
axios.get(`http://localhost:3001/memos/${id}`).then((res) => {
setPost(res.data);
setLoading(false);
});
};
useEffect(() => {
console.log("hello");
getPost(id);
}, [id]);
useParams 를 이용해 url 의 id 부분을 가져올 수 있다.
데이터를 요청하는 함수를 만들고 useEffect 를 통해 실행시키는 과정은 List 를 불러오는 것과 동일하여 설명을 생략하겠다.
아 이번엔 useEffect 에 빈배열이 아니라 id 를 넣어서 id 가 변경될 때마다 useEffect 함수가 실행되도록 했다.
이걸 의존성 배열안에 값을 넣는다. 라고 표현하는 것 같다.
메모를 생성할 때 작성일을 아래와 같이 넣어 저장했다.
createdAt: Date.now() //저장 : "createdAt": 1671310331099
이러면 우리가 인식하기 어려운 숫자로 저장이 되는데,
이 데이터를 우리가 읽기 쉽도록 printDate 함수를 만들었다.
const printDate = (timestamp) => {
return new Date(timestamp).toLocaleString();
}; //작성일 : 2022. 12. 18. 오전 5:52:11
▶ 프로젝트를 마치며
재밌다 ! 리액트 !
역시 어려워서 싫었던 것이 아니라 익숙하지 않아서 싫었던거슬 ..
새로운거 배울때 제일 중요한거 ~ 열린마음 ~ 마음 열어 ~
github : https://github.com/HAN-SEOHYUN/react-memo
참고
'React' 카테고리의 다른 글
[React 프로젝트] 프로젝트 생성 / 발생에러 및 해결 (0) | 2022.11.28 |
---|---|
[React] map / key (0) | 2022.07.06 |
[React] useEffect (0) | 2022.07.01 |
[React] useConext + Context API (0) | 2022.06.30 |
[React] useRef (0) | 2022.06.28 |