본문으로 건너뛰기

안동민 개발노트

본문 시작
8장 : 비동기 처리 및 데이터 페칭Axios 게시판 CRUD

게시물 CRUD 컴포넌트

게시물 목록·상세·작성 컴포넌트에 Axios 요청과 로딩·오류 상태를 연결합니다.

게시물 관련 컴포넌트

PostList.jsx

게시물 목록을 조회하고 삭제 버튼을 포함합니다.

src/components/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

특정 게시물 상세 내용을 조회합니다.

src/components/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

새 게시물을 추가하는 폼입니다.

src/components/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;