본문 바로가기
LG CNS AM INSPIRE CAMP

<LG CNS 5기> 9일차 TIL : 토이프로젝트- useEffect 기반 데이터 바인딩과 useParams 동적 라우팅 패턴 분석

by hihijh826 2026. 6. 1.
728x90
반응형
SMALL

학습일자 : 2026.05.29

 

 

💡 PART 1. 핵심 기술 개념별 코드 매칭

 

1. useEffect 무한 루프 방지 코드

개념: 컴포넌트 마운트 시점에 서버 데이터 요청을 단 한 번만 실행하도록 제한한다.

// BlogMainPage.jsx 내부
useEffect(() => {
    const loadData = async () => {
        try {
            const response = await api.get('blogs'); // 서버 요청
            setBlogs(response.data);                // 상태 변경 -> (배열이 없으면 무한 리렌더링 유발)
        } catch (error) { ... }
    };
    loadData();
}, []); // 💡 핵심: 이 빈 배열 덕분에 무한 루프에 빠지지 않고 딱 1번만 실행됨!

 

 

2. 인증 및 인가 상태 확인 코드

개념: localStorage에 저장된 유저 토큰을 읽어와 권한을 검증하고 UI를 제어한다

// BlogMainPage.jsx & BlogReadPage.jsx 공통
const token = localStorage.getItem('token'); // 💡 인증 정보(토큰) 공유(get)

// JSX 렌더링 영역
{token && <WelcomeMessage>{token} 님 환영합니다.</WelcomeMessage>}
// 💡 인가 렌더링: 토큰이 존재할(true) 때만 환영 메시지를 화면에 렌더링함

 

 

3. 동적 라우팅과 useParams 활용 코드

개념: 주소창의 가변적인 ID 값을 프론트엔드 변수로 추출한다.

// BlogReadPage.jsx 내부
const { id } = useParams();
// 💡 만약 브라우저 주소가 /blog/3 이라면 id 변수에 "3"이 자동으로 할당됨

console.log(`debug >>>> BlogReadPage mount params value : ${id}`);
    <Route path="/blog/read/:id" element={<BlogReadPage />} />

-> 이렇게 뒤에  : 을 붙여 동적 라우팅을 진행

 

4. Path Variable vs Query String 실무 코드

개념: 식별자는 경로 변수로, 조건(필터링)은 쿼리 스트링으로 요청

// BlogReadPage.jsx 내부 (실무 정석 분리 호출 방식)

// 1. Path Variable 방식 (특정 게시글 '하나'를 식별)
const blogResponse = await api.get(`/blogs/${id}`);

// 2. Query String 방식 (? 뒤에 조건 제시: 이 게시글 id에 달린 댓글들만 '필터링')
const commentResponse = await api.get(`/comments?blogId=${id}`);

 

 

5. map()을 이용한 하향식 데이터 전달 (props)

개념: 부모의 배열 데이터를 순회하며 자식 컴포넌트를 반복 생성하고 데이터를 넘겨줌.

// BlogList.jsx 내부
const BlogList = ({ blogs }) => { // 부모(MainPage)에게 props로 blogs 배열을 받음
  return (
    <Wrapper>
      {blogs.map((blog, index) => ( // 💡 map()으로 배열 순회
          <BlogItem key={index} blog={blog} /> // 💡 자식(BlogItem)에게 개별 객체를 props로 전달
      ))}
    </Wrapper>
  )
}

 

 

 

6. _embed 옵션과 프론트/백엔드 역할 분담 

 

🏢 ① 실제 실무 환경 (진짜 백엔드가 있을 때)

  • 프론트엔드의 역할: 주소에 잡다한 조건 없이 아주 깔끔하게 필요한 자원만 호출함. (api.get('/blogs/1'))
  • 백엔드의 역할: 서버 프로그램(Java, Node.js 등)이 요청을 받아 데이터베이스(DB) 내의 테이블들을 JOIN(결합)함. 게시글과 댓글을 하나의 깔끔한 JSON 덩어리로 조립하여 프론트엔드로 보내주기에 프론트엔드는 주는 대로 받아서 화면에 그리기만 하면 되는 구조
 useEffect(() => {
        const loadData = async () => {
            try {

                //   (API 2번 분리 호출)
                // 1. 게시글 정보를 먼저 가져옵니다.
                const blogResponse = await api.get(`/blogs/${id}`);
                setBlog(blogResponse.data);
                // 2. 해당 게시글 id와 일치하는(blogId) 댓글들만 따로 검색해서 가져옵니다.
                const commentResponse = await api.get(`/comments?blogId=${id}`);
                setComments(commentResponse.data || []);
            } catch (error) {
                console.error("데이터를 불러오는데 실패했습니다.", error);
            }
        };
        loadData();
    }, [id]);

 

 

📦 ② 현재 학습 환경 (json-server만 있을 때)

  • 문제점: 데이터를 예쁘게 조립해 줄 백엔드 개발자(서버 로직)가 없고, 단순한 가짜 DB 파일(db.json)만 존재
  • 해결책: 프론트엔드인 우리가 json-server에게 "진짜 백엔드 서버가 해주는 것처럼, 너도 게시글 줄 때 댓글 찾아서 하나로 묶어서 줘봐!"라고 특수한 명령을 내려야 하는데 그 특수한 명령어가 바로 ?_embed=comments인 것.
// json-server 환경에서 관계형 데이터를 한 번에 묶어 가져오는 특수 치트키
await api.get(`blogs/${id}?_embed=comments`)
    .then(response => {
        // 결과물인 response.data 구조 자체는 실무에서 백엔드가 조립해 준 데이터 구조와 완벽히 일치함!
        setBlog(response.data);
        setComments(response.data.comments);
    });

 

 

 

🏢 PART 2. 각 페이지별 핵심 역할 코드 매칭

 

1️⃣ BlogMainPage.jsx (데이터 뿌리기 및 로그아웃)

역할: 전체 목록 조회 및 로컬 스토리지 비우기

// 1. 데이터 조회 후 상태 저장
const [blogs, setBlogs] = useState([]);
setBlogs(response.data); // 서버에서 가져온 전체 배열을 담음

// 2. 로그아웃 핸들러
const logoutHandler = () => {
    localStorage.removeItem('token'); // 로컬스토리지에서 토큰 삭제 (인증 해제)
    navigate('/');                     // 로그인/화면 첫 페이지로 튕겨냄
};

 

 

2️⃣ BlogReadPage.jsx (예외 처리, 로딩 UI 및 댓글 생성 시스템)

 

 

 

역할: 비동기 데이터 방어, 관계형 데이터 연동, 그리고 스프레드 연산자를 이용한 댓글 실시간 추가

  • 기존 코드 (데이터 방어 및 로딩 UI):
<PostContainer>
    {/* 💡 비동기 특성상 데이터가 서버에서 오기 전(null)에 접근하면 에러가 나므로 삼항 연산자로 방어 */}
    {blog ? (
        <>
            <TitleText>{blog.title}</TitleText>
            <ContentText>{blog.content}</ContentText>
        </>
    ) : (
        <TitleText>로딩 중...</TitleText> // 데이터가 도착하기 전 보여주는 화면
    )}
</PostContainer>
  • 댓글 비동기 생성 및 상태 업데이트:
// 1. 댓글 입력창 양방향 바인딩 (제어 컴포넌트)
const [comment, setComment] = useState('');
<TextInput value={comment} changeHandler={(e) => setComment(e.target.value)} />

// 2. 댓글 POST 요청 및 리액트 불변성(Immutability) 유지 상태 업데이트
const commentHandler = async () => {
    try {
        const response = await api.post('/comments', {
            blogId: id,       // 💡 어떤 게시글에 달린 댓글인지 Path Variable로 받아온 id 매칭
            content: comment, // 입력된 댓글 내용
            email: email,     // 작성자 정보
        });

        // 💡 중요: 리액트의 '불변성 원칙'을 지키기 위해 기존 댓글 배열을 얕은 복사([...comments])하고,
        // 서버에서 생성 완료되어 반환된 새 댓글 데이터(response.data)를 뒤에 붙여 화면을 실시간 갱신함.
        setComments([...comments, response.data]);
        setComment(''); // 입력창 초기화
    } catch (error) {
        console.error("댓글 작성에 실패했습니다.", error);
    }
};

 

3️⃣ BlogWritePage.jsx (제어 컴포넌트 입력 및 상태 코드 검증)

역할: 입력값 양방향 바인딩 및 201 상태 코드를 통한 성공 유무 분기

// 1. 제어 컴포넌트 상태 관리 및 입력 핸들러
const [title, setTitle] = useState('');
<TextInput value={title} changeHandler={(e) => setTitle(e.target.value)} />

// 2. 서버에 데이터 생성(POST) 및 응답 코드 검증
const response = await api.post('blogs', { title, content, email });

if (response.status === 201) { // 💡 201(Created): 서버에 데이터 저장 성공 시
    navigate('/blog/index');    // 메인 목록 페이지로 이동
} else {
    setErrorMsg('글 작성에 실패했습니다.'); // 실패 시 에러 메시지 세팅
}

 

 

 

오늘의 회고:그동안 파편적으로 배우던 리액트의 문법들을 모아 하나의 '블로그 토이 프로젝트'를 완성하면서 웹 서비스의 핵심 CRUD 흐름을 이해 한 것 같다. useEffect와 useParams를 활용해 무한 루프 없이 특정 게시글 데이터를 단방향으로 안전하게 바인딩하고, 진짜 백엔드가 없는 한계를 _embed 옵션으로 극복해 보며 프론트와 백엔드의 명확한 역할 분담을 체득할 수 있었다. 특히 새 글을 저장할 때의 201 상태 코드 검증과 댓글 작성 시 리액트의 불변성 원칙을 지키기 위해 스프레드 연산자(...comments)로 배열을 복사해 UI를 실시간 갱신하는 일련의 제어 과정을 직접 내 손으로 구현해 내며 실무형 데이터 설계의 재미를 느끼게 된 것 같다.

728x90
반응형
LIST