8장 : 비동기 처리 및 데이터 페칭Axios 게시판 CRUD
게시물 CRUD 컴포넌트
게시물 목록·상세·작성 컴포넌트에 Axios 요청과 로딩·오류 상태를 연결합니다.
게시물 관련 컴포넌트
PostList.jsx
게시물 목록을 조회하고 삭제 버튼을 포함합니다.
import { Link } from 'react-router-dom';
import LoadingSpinner from './LoadingSpinner';
import ErrorDisplay from './ErrorDisplay';
function PostList({ posts, loading, error, onDelete, onRetry }) {
if (loading) {
return <LoadingSpinner />;
}
if (error) {
return <ErrorDisplay error={error} onRetry={onRetry} />;
}
if (!posts || posts.length === 0) {
return <div style={{ textAlign: 'center', padding: '20px', color: '#666' }}>게시물이 없습니다.</div>;
}
return (
<div style={{ marginTop: '30px' }}>
<h2 style={{ marginBottom: '20px', color: 'var(--header-color)' }}>전체 게시물 ({posts.length}개)</h2>
<ul style={{ listStyle: 'none', padding: 0 }}>
{posts.map(post => (
<li
key={post.id}
style={{
padding: '15px',
marginBottom: '10px',
border: '1px solid var(--card-border)',
borderRadius: '5px',
backgroundColor: 'var(--card-bg)',
boxShadow: '0 1px 3px rgba(0,0,0,0.02)',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
}}
>
<Link to={`/posts/${post.id}`} style={{ textDecoration: 'none', color: 'var(--text-color-main)', flexGrow: 1 }}>
<h3 style={{ margin: '0 0 5px 0', color: '#3498db', fontSize: '1.2em' }}>{post.title}</h3>
<p style={{ margin: 0, fontSize: '0.9em', color: '#777' }}>작성자 ID: {post.userId}</p>
</Link>
<button
onClick={() => onDelete(post.id)}
className="button danger"
style={{ padding: '8px 15px', fontSize: '0.8em' }}
>
삭제
</button>
</li>
))}
</ul>
</div>
);
}
export default PostList;PostDetail.jsx
특정 게시물 상세 내용을 조회합니다.
import LoadingSpinner from './LoadingSpinner';
import ErrorDisplay from './ErrorDisplay';
import { Link } from 'react-router-dom';
function PostDetail({ post, loading, error, onRetry }) {
if (loading) {
return <LoadingSpinner />;
}
if (error) {
return <ErrorDisplay error={error} onRetry={onRetry} />;
}
if (!post) {
return <div style={{ textAlign: 'center', padding: '20px', color: '#666' }}>게시물을 찾을 수 없습니다.</div>;
}
return (
<div style={{ maxWidth: '700px', margin: '20px auto', padding: '30px', border: '1px solid var(--card-border)', borderRadius: '8px', boxShadow: '0 2px 8px rgba(0,0,0,0.08)', backgroundColor: 'var(--card-bg)' }}>
<h2 style={{ color: '#3498db', marginBottom: '15px' }}>{post.title}</h2>
<p style={{ fontSize: '1.1em', lineHeight: '1.8' }}>{post.body}</p>
<p style={{ fontSize: '0.9em', color: '#888', marginTop: '20px' }}>작성자 ID: {post.userId}</p>
<Link to="/posts" className="button" style={{ marginTop: '20px' }}>
목록으로 돌아가기
</Link>
</div>
);
}
export default PostDetail;AddPostForm.jsx
새 게시물을 추가하는 폼입니다.
import { useState } from 'react';
import LoadingSpinner from './LoadingSpinner';
import ErrorDisplay from './ErrorDisplay';
function AddPostForm({ onAddPost, loading, error }) {
const [title, setTitle] = useState('');
const [body, setBody] = useState('');
const handleSubmit = (e) => {
e.preventDefault();
if (!title.trim() || !body.trim()) {
alert('제목과 내용을 입력해주세요.');
return;
}
onAddPost({ title, body, userId: 1 }); // 예시로 userId 1로 설정
setTitle('');
setBody('');
};
return (
<div style={{ padding: '25px', border: '1px solid var(--card-border)', borderRadius: '8px', backgroundColor: 'var(--card-bg)', boxShadow: '0 2px 5px rgba(0,0,0,0.03)', marginTop: '40px' }}>
<h2 style={{ marginBottom: '20px', color: 'var(--header-color)' }}>새 게시물 추가</h2>
<form onSubmit={handleSubmit}>
<div style={{ marginBottom: '15px' }}>
<label htmlFor="title" style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold', color: 'var(--text-color-main)' }}>제목:</label>
<input
type="text"
id="title"
value={title}
onChange={(e) => setTitle(e.target.value)}
disabled={loading}
style={{ width: '100%', padding: '10px', border: '1px solid #ccc', borderRadius: '4px', boxSizing: 'border-box' }}
/>
</div>
<div style={{ marginBottom: '15px' }}>
<label htmlFor="body" style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold', color: 'var(--text-color-main)' }}>내용:</label>
<textarea
id="body"
value={body}
onChange={(e) => setBody(e.target.value)}
disabled={loading}
rows="5"
style={{ width: '100%', padding: '10px', border: '1px solid #ccc', borderRadius: '4px', boxSizing: 'border-box', resize: 'vertical' }}
></textarea>
</div>
{loading && <LoadingSpinner />}
{error && <ErrorDisplay error={error} />}
<button type="submit" className="button success" disabled={loading}>
{loading ? '추가 중...' : '게시물 추가'}
</button>
</form>
</div>
);
}
export default AddPostForm;