
2025 / 04 / 04
오늘 수업 시간에는 {JSON} Paceholder를 사용하여 API 호출 연습을 하였습니다.
API 호출을 하면서.. 그냥 이해가 안되는 부분이 너무 많았습니다. 쿼리 파라미터를 사용해서 필터링을 구현했는데... 어렵습니다.. 어떡하죠.. 일단 구현한 순서대로
이해하기 쉽도록 정리해서 작성해보도록 하겠습니다. 오늘 내용이 젤 어려웠습니다.
- React를 사용해서 만든 간단한 블로그입니다.
- 사용자 정보를 기반으로 게시글을 검색할 수 있습니다.
- 카드 형식 게시글을 클릭 시 모달창으로 상세 내용을 볼 수 있습니다.
- App.jsx (전체 앱 로직 담당 (데이터 처리 중심))
- SearchBox.jsx (검색 입력 UI)
- BlogPost.jsx (게시글 카드 UI)
- Modal.jsx (모달창 (상세 내용 보기))
1) 사용자와 게시글 데이터를 외부 API에서 가져옵니다.
2) 사용자가 검색어 입력 -> userId 또는 이름으로 게시글 필터링합니다.
3) 게시글 목록 렌더링 -> 클릭 시 모달로 상세 내용을 표시합니다.
앱의 중심, 데이터 처리와 UI 연결
- 전체 앱 구조, 데이터 로딩, 검색 처리를 담당하는 컴포넌트입니다.
- useEffect로 사용자 & 게시글 API 데이터를 불러옵니다.
- 검색어에 따라 조건을 다르게 API를 요청합니다.(userId or 이름)
- 게시글 클릭 시 모달로 상세 보기가 가능합니다.
import React, { useEffect, useState } from "react"; import "./App.css"; import BlogPost from "./components/BlogPost"; import Modal from "./components/Modal"; import SearchBox from "./components/SearchBox"; function App() { const [users, setUsers] = useState([]); // 사용자 목록 저장 const [filteredPosts, setFilteredPosts] = useState([]); // 게시글 목록 const [isDialogOpen, setIsDialogOpen] = useState(false); // 모달 열림 여부 const [selectedPost, setSelectedPost] = useState(null); // 선택된 게시글 const [searchTerm, setSearchTerm] = useState(""); // 검색어 useEffect(() => { const fetchData = async () => { try { // 1. 사용자 데이터 먼저 가져오기 const usersResponse = await fetch("https://jsonplaceholder.typicode.com/users"); const usersData = await usersResponse.json(); setUsers(usersData); // 2. 검색어에 따라 게시글 요청 URL 결정 let postsUrl = "https://jsonplaceholder.typicode.com/posts"; if (searchTerm && !isNaN(searchTerm)) { postsUrl += `?userId=${searchTerm}`; // 숫자면 userId로 검색 } else if (searchTerm) { const user = usersData.find((user) => user.name.toLowerCase().includes(searchTerm.toLowerCase()) ); if (user) { postsUrl += `?userId=${user.id}`; // 이름 검색 시 해당 userId 사용 } } // 3. 게시글 데이터 가져오기 const postsResponse = await fetch(postsUrl); const postsData = await postsResponse.json(); setFilteredPosts(postsData); } catch (error) { console.error("데이터 가져오기 실패:", error); } }; fetchData(); // 실행 }, [searchTerm]); // 검색어가 바뀔 때마다 실행됨 // 게시글 클릭 시 모달 열기 const openDialog = (post) => { setSelectedPost(post); setIsDialogOpen(true); }; // 모달 닫기 const closeDialog = () => { setIsDialogOpen(false); setSelectedPost(null); }; // 검색어 입력 시 상태 변경 const handleSearch = (value) => { setSearchTerm(value); }; return ( <div className="body"> <div className="container"> <div className="header"> <h2>Blog</h2> <SearchBox onSearch={handleSearch} /> </div> <hr /> <div className="BlogInfo"> {filteredPosts.map((post) => ( <BlogPost key={post.id} post={post} users={users} onClick={openDialog} /> ))} </div> {/* 모달 표시 */} {isDialogOpen && selectedPost && ( <Modal post={selectedPost} users={users} onClose={closeDialog} /> )} </div> </div> ); } export default App;
useState( )로 상태 정의
- useState( )는 컴포넌트 내에서 값을 기억해두는 역할을 합니다.
- 화면을 다시 그릴 때도 값을 유지합니다.
const [users, setUsers] = useState([]); const [filteredPosts, setFilteredPosts] = useState([]); const [isDialogOpen, setIsDialogOpen] = useState(false); const [selectedPost, setSelectedPost] = useState(null); const [searchTerm, setSearchTerm] = useState("");
useEffect( )와 API
- await fetch(...) → API에서 데이터를 불러오는 비동기 함수입니다.
- set변수명(...)을 사용해야 React가 화면을 다시 렌더링합니다.
useEffect(() => { const fetchData = async () => { const usersResponse = await fetch("https://.../users"); const usersData = await usersResponse.json(); setUsers(usersData); let postsUrl = "https://.../posts"; // 검색어 처리 if (searchTerm && !isNaN(searchTerm)) { postsUrl += `?userId=${searchTerm}`; } else if (searchTerm) { const user = usersData.find((user) => user.name.toLowerCase().includes(searchTerm.toLowerCase()) ); if (user) { postsUrl += `?userId=${user.id}`; } } const postsResponse = await fetch(postsUrl); const postsData = await postsResponse.json(); setFilteredPosts(postsData); }; fetchData(); }, [searchTerm]);
모달을 열고 닫는 기능 구현
- 게시글을 클릭하면 openDialog( ) 실행 -> 선택된 게시글을 저장하고 모달을 엽니다.
- 모달 닫기 버튼을 누르면 closeDialog( )로 상태를 초기화합니다.
const openDialog = (post) => { setSelectedPost(post); setIsDialogOpen(true); }; const closeDialog = () => { setSelectedPost(null); setIsDialogOpen(false); };
게시글 카드 UI
- 블로그 카드 하나를 보여주는 컴포넌트입니다.
- 블로그 카드 UI 및 클릭 이벤트 처리를 담당합니다.
- props로 postm users, onClick을 받아서 사용합니다.
- 클릭 시 상위 컴포넌트(App)에서 모달이 열리도록 onClick(post)를 호출합니다.
- 작성자 이름은 post.userId로 찾아서 표시합니다.
import React from "react"; import Img from "../assets/img.jpg"; import styled from "styled-components"; const BlogBox = styled.div` width: 300px; height: 400px; background-color: #f9f9f9; margin: 10px; border: 1px solid #ddd; border-radius: 10px; padding: 20px; box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); cursor: pointer; transition: transform 0.3s ease; &:hover { transform: translateY(-5px); background-color: rgb(157, 187, 187); } `; const BlogPost = ({ post, users, onClick }) => { const author = users.find((user) => user.id === post.userId); return ( <BlogBox onClick={() => onClick(post)}> <img src={Img} alt="Blog" style={{ width: "100%", height: "50%", objectFit: "cover", borderRadius: "8px" }} /> <div className="blog_text" style={{ padding: "10px", backgroundColor: "white", borderRadius: "8px" }}> <h4 className="title">{post.title}</h4> <p>{post.body}</p> {author && <p>작성자 : {author.name}</p>} </div> </BlogBox> ); }; export default BlogPost;
- 각 게시글(post)에는 userId만 있습니다.
- 이름을 표시하려면 users 배열에서 찾아야하는 구조입니다.
- find( )는 조건을 만족하는 첫 번째 요소를 반환합니다.
- 값이 없을 수도 있기 때문에 null 체크를 하는 것이 좋습니다.
(author && author.name 또는 author?.name)const author = users.find((user) => user.id === post.userId);
- onClick(post) 클릭 시 App.jsx로 게시글의 정보를 전달합니다.
- 게시글 제목, 본문, 작성자를 표시하고 있습니다.
- 이미지/글 hover 효과는 styled-components로 스타일링하였습니다.
<BlogBox onClick={() => onClick(post)}> <img src={Img} alt="Blog" /> <div className="blog_text"> <h4 className="title">{post.title}</h4> <p>{post.body}</p> {author && <p>작성자 : {author.name}</p>} </div> </BlogBox>
검색 입력창
- 검색어 입력 UI 및 이벤트를 담당합니다.
- 사용자 입력 값을 App 컴포넌트로 전달합니다.
- img 클릭 시 검색을 실행합니다.
- 숫자 or 이름 모두 입력 가능합니다.
import React, { useState, useRef } from "react"; import search from "../assets/icon.png"; import styled from "styled-components"; const Search_Box = styled.div` display: flex; align-items: center; margin-bottom: 20px; & input { padding: 5px 10px; font-size: 16px; margin-right: 10px; } & img { width: 24px; height: 24px; cursor: pointer; } `; function SearchBox({ onSearch }) { const [userInput, setUserInput] = useState(""); const searchRef = useRef(""); const handleChange = (e) => { setUserInput(e.target.value); }; const handleSearchClick = () => { searchRef.current.focus(); onSearch(userInput); // 상위(App)로 검색어 전달 }; return ( <Search_Box> <input type="text" placeholder="검색..." ref={searchRef} value={userInput} onChange={handleChange} /> <img src={search} alt="검색" onClick={handleSearchClick} /> </Search_Box> ); } export default SearchBox;
- 입력창에 입력한 값은 userInput이라는 상태에 저장됩니다.
const [userInput, setUserInput] = useState(""); const handleChange = (e) => { setUserInput(e.target.value); };
- onSearch( )는 부모로 전달된 함수입니다.(App에서 searchTerm 업데이트)
- ref를 써서 포커스 이동도 가능합니다.
- React에서는 부모 -> 자식으로만 데이터를 전달할 수 있습니다.(props)
- 자식 -> 부모는 콜백 함수를 통해 전달해야합니다.
const handleSearchClick = () => { searchRef.current.focus(); onSearch(userInput); // App.js에 검색어 전달 };
모달창 컴포넌트
- 게시글 상세 모달을 띄우는 역할을 합니다.
- post와 users를 받아서 게시글의 상세 내용을 표시합니다.
- 배경 흐림(blur) 효과와 함께 모달창을 디자인하였습니다.
- 버튼 클릭 시 onClose( ) 호출로 모달이 닫히게 됩니다.
import React from "react"; import styled from "styled-components"; const Modalcss = styled.div` position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.6); display: flex; justify-content: center; align-items: center; & .modal { background-color: white; border-radius: 8px; padding: 20px; width: 90%; max-width: 800px; } & button { margin-top: 20px; border: none; background: rgb(157, 187, 187); padding: 8px 16px; border-radius: 5px; cursor: pointer; } `; function Modal({ post, users, onClose }) { const author = users.find((user) => user.id === post.userId); return ( <Modalcss> <div className="modal"> <h3>{post.title}</h3> <p>{post.body}</p> <p>작성자: {author?.name}</p> <button onClick={onClose}>닫기</button> </div> </Modalcss> ); } export default Modal;
- BlogPost.jsx와 같습니다.
const author = users.find((user) => user.id === post.userId);
- 모달에는 게시글 제목, 본문, 작성자를 표시하고 있습니다.
- 닫기 버튼을 클릭하면 onClose( )를 호출하여 모달이 사라집니다.
- 조건부 렌더링 은 App.jsx에서 처리되고 있습니다.
<Modalcss> <div className="modal"> <h3>{post.title}</h3> <p>{post.body}</p> <p>작성자: {author?.name}</p> <button onClick={onClose}>닫기</button> </div> </Modalcss>
- SearchBox -> 검색어 입력
- App.jsx -> searchTerm 변경 -> useEffect로 데이터 다시 불러오기
- filteredPosts 상태로 BlogPost 카드 렌더링
- BlogPost 클릭 -> selextedPost 설정 + 모달 열림
- Modal.jsx -> 상세 내용 표시
App 컴포넌트 렌더링 (시작점)
- useEffect 안에서 처음 데이터를 불러오며 시작됩니다.
- searchTerm 상태가 변경될 때마다 데이터를 새로 불러옵니다.
useEffect(() => { fetchData(); // 사용자와 게시글 불러오기 }, [searchTerm]);
사용자 및 게시글 데이터 불러오기
- 사용자 전체 목록(users)를 먼저 가져옵니다.
- 검색어(searchTerm)가 있으면, 조건에 맞게 posts만 필터링해서 가져옵니다.
- 숫자면 userId로 필터링합니다.
- 문자면 이름을 포함한 userId를 찾아서 필터링합니다.
- 이름으로 검색하는 경우에도, 결국 userId로 바꿔서 API를 호출했습니다.
const usersResponse = await fetch("https://.../users"); const postsResponse = await fetch("https://.../posts");
- SearchBox 컴포넌트에서 텍스트 입력 후 아이콘을 클릭하면 onSearch( )가 호출됩니다.
- 부모(App)에서 searchTerm 상태 변경됨 → useEffect 재실행 → 게시글 다시 불러옴
const handleSearch = (value) => { setSearchTerm(value); // 상태만 바꾸면 자동으로 데이터 다시 가져옴 };
BlogPost 컴포넌트
- 필터링된 게시글 배열 filteredPosts를 .map( )으로 렌더링합니다.
- 각 post를 BlogPost에 props로 넘겨줍니다.
- 작성자는 post.userId로 users 배열에서 찾습니다.
- 사용자 데이터(users)가 없으면 작성자 이름이 안 뜸 → fetch 순서가 중요합니다.
const author = users.find((user) => user.id === post.userId);
상세 보기 기능
- 카드 클릭 시 → openDialog(post) 호출 → selectedPost 상태 변경
- 조건부 렌더링으로 가 보이게 됩니다.
- 모달 내에서도 users에서 작성자 이름을 찾아서 보여줍니다.
- 닫기 버튼을 클릭하면 onClose( )를 실행하여 모달이 닫기게 됩니다.
{isDialogOpen && selectedPost && ( <Modal post={selectedPost} users={users} onClose={closeDialog} /> )}
- useEffect의 실행 시점
- useEffect(() => {...}, [searchTerm])은 처음 한 번 + 검색어 변경될 때만 실행
- 검색이 이루어질 때마다 데이터를 새로 받아오는 구조
- 비동기 흐름
- 사용자 데이터를 먼저 받아야, 이름으로 검색할 수 있음
- 사용자 정보를 받아오기 전에 검색어를 기준으로 post API를 요청하면 오류 날 수 있음
- 이걸 방지하기 위해 항상 users를 먼저 받아오는 구조로 설계되어 있음
- 모달 조건부 렌더링
{isDialogOpen && selectedPost && ( <Modal ... /> )}
- 모달은 항상 렌더링되는 게 아님
- 상태 값 두 개가 모두 true일 때만 화면에 표시됨
- 위에 코드를 실행한 결과입니다.
62일차 후기
- API를 호출해 사용하는 것은 어느정도 익숙해진 것 같은데... 쿼리 파라미터를 활용해 검색 필터링을 구현하는 것이 조금 어려웠습니다. (๑ -`. ‘- ๑)/
- filter를 사용해서 구현하면 될 텐데 쿼리 파라미터를 사용하면 뭐가 더 나은지 궁금해서 만들어보고 찾아본 결과 확실히.. 쿼리 파라미터를 사용하니까 속도가 빨랐습니다.
- 컴포넌트가 많아질수록 헷갈리는 부분이 생기는 것 같아서 걱정입니다.. (꜆꜄ᴗ͈﹏ᴗ͈)꜆꜄꜆