# [TIL] 6. React: 블로그 만들기

MinseoKim·2026년 8월 6일
post-thumbnail

1. 오늘의 한 줄 요약

오늘은 props.title로 값을 꺼내려다가 화면에 아무것도 뜨지 않는 버그를 만났다.

원인은 부모 컴포넌트가 title을 직접 넘긴 것이 아니라, blog라는 이름의 props에 객체 전체를 담아서 전달하고 있었기 때문이었다.

props의 이름과 구조는 받는 쪽에서 마음대로 정하는 것이 아니라, 부모가 어떤 이름과 형태로 넘겼는지를 따라가야 한다.

직접 버그를 만나고 나니 props의 구조가 훨씬 명확하게 이해됐다.


2. 오늘 배운 내용

1) 구조분해할당 (Destructuring)

구조분해할당은 객체나 배열 안에 있는 값을 꺼내서 변수로 만드는 문법이다.

객체 구조분해

// 구조분해 없이
const user = { name: '김민서', age: 25 };

const name = user.name;
const age = user.age;

구조분해할당을 사용하면 이렇게 줄일 수 있다.

const { name, age } = user;

// name = '김민서'
// age = 25

여기서 중요한 점은 객체 구조분해의 경우 변수명이 객체의 key 이름과 같아야 한다는 것이다.

const user = {
    name: '김민서',
    age: 25
};

const { nickname } = user;

console.log(nickname); // undefined

user 객체 안에는 nickname이라는 key가 없기 때문에 undefined가 나온다.

배열 구조분해

배열은 객체와 조금 다르다.

객체가 key 이름을 기준으로 값을 가져온다면, 배열은 순서를 기준으로 값을 가져온다.

const arr = ['사과', '바나나'];

const [first, second] = arr;

// first = '사과'
// second = '바나나'

그래서 React에서 자주 사용하는 useState()도 배열 구조분해를 사용한다.

const [cnt, setCnt] = useState(0);

useState()가 항상

  1. 현재 상태값
  2. 상태를 변경하는 함수

순서로 값을 반환하기 때문에 가능한 방식이다.

함수 매개변수에서도 구조분해 가능

기존에는 이렇게 작성할 수 있다.

function greet(props) {
    const name = props.name;
}

하지만 매개변수 자리에서 바로 구조분해할 수도 있다.

function greet({ name }) {
}

React 컴포넌트에서 props를 받을 때 자주 사용하게 되는 패턴이다.


2) .map() — 배열을 화면 목록으로 바꾸기

.map()은 배열의 각 원소를 하나씩 꺼내 특정 작업을 수행한 뒤, 그 결과를 모아 새로운 배열을 반환하는 함수다.

const numbers = [1, 2, 3];

const doubled = numbers.map((n) => n * 2);

console.log(doubled);
// [2, 4, 6]

중요한 점은 .map()이 기존 배열을 직접 수정하는 것이 아니라는 것.

console.log(numbers);
// [1, 2, 3]

원본 배열은 그대로 유지된다.

React에서 .map()을 많이 사용하는 이유

이번 실습에서는 카테고리 배열을 화면의 탭 컴포넌트로 변환하는 데 사용했다.

{categories.map((category) => (
    <CategoryTab
        key={category}
        className={activeCategory === category ? 'active' : undefined}
        onClick={() => setActiveCategory(category)}
    >
        {category}
    </CategoryTab>
))}

만약 배열이 다음과 같다면,

const categories = ['frontend', 'backend'];

.map()은 각 데이터를 하나씩 꺼내서 React 컴포넌트로 바꾼다.

개념적으로는 이런 배열이 만들어지는 셈이다.

[
    <CategoryTab key="frontend">
        frontend
    </CategoryTab>,

    <CategoryTab key="backend">
        backend
    </CategoryTab>
]

그리고 React가 이 컴포넌트 배열을 순서대로 화면에 렌더링한다.

.map()데이터 배열을 화면에 그릴 컴포넌트 배열로 변환할 때 사용한다.


3) &&를 이용한 조건부 렌더링

JavaScript의 && 연산자는 왼쪽 값이 참일 때만 오른쪽 값을 확인한다.

true && '보여줄 값'
// → '보여줄 값'

false && '보여줄 값'
// → false

이 특성을 React의 조건부 렌더링에 활용할 수 있다.

{user && (
    <WelcomeMessage>
        {user}님 환영합니다.
    </WelcomeMessage>
)}

이 코드는 쉽게 말하면 다음과 같다.

"user가 있으면 환영 메시지를 보여줘."

user가 존재하면 <WelcomeMessage>가 렌더링된다.

반대로 usernull, undefined, false라면 뒤의 컴포넌트를 실행하지 않는다.

React는 다음 값들을 화면에 렌더링하지 않는다.

null
undefined
false

따라서 로그인하지 않아 user가 없다면 환영 메시지 자체가 화면에 나타나지 않는다.


3. 실습 결과

이번 실습에서는 다음 기능까지 구현했다.

  • 회원가입
  • 로그인
  • 블로그 목록 페이지 /blogs/index
  • 로그인한 사용자 이름 표시
  • 글 작성하기 버튼
  • 로그아웃 버튼
  • 전체 / front-end / back-end 카테고리 필터링


회원가입을 하면 data.jsonusers 안에 신규 사용자 정보가 정상적으로 추가되는 것도 확인했다.

status: 201

회원가입 성공 후에는 로그인 페이지로 자동 이동한다.

콘솔에서도 201 응답을 확인했다.

블로그 목록 페이지에서는 axios를 통해 /blogs API도 정상적으로 호출됐다.

data: Array(2)
status: 200

즉, 서버에서 블로그 목록 데이터를 받아오는 것까지 정상적으로 동작했다.

로그인한 사용자에 따라 사용자 정보도 다르게 불러오도록 구현했다.

실습에서는 사용자 이메일을 화면에 출력했는데, 나는 localStorage를 추가해서 이메일 대신 사용자 이름이 나오도록 살짝 수정했다.


4. 문제와 해결

문제: props.title이 화면에 나오지 않았다

BlogItem.jsx에서 블로그 제목을 출력하려고 다음과 같이 작성했다.

const BlogItem = (props) => {
    return (
        <Wrapper>
            <TitleText>
                {props.title}
            </TitleText>
        </Wrapper>
    );
};

그런데 오류 메시지도 없이 제목이 화면에 나타나지 않았다.

처음에는 props.title로 바로 가져오면 되는 줄 알았다.

부모 컴포넌트를 확인해보니

BlogList.jsx에서 BlogItem을 호출하는 부분은 다음과 같았다.

const BlogList = (props) => {
    return (
        <Wrapper>
            {
                props.ary && props.ary.length > 0
                    ? props.ary.map((blog, idx) => {
                        return (
                            <BlogItem
                                key={idx}
                                blog={blog}
                            />
                        );
                    })
                    : '등록된 글이 없습니다.'
            }
        </Wrapper>
    );
};

여기서 중요한 부분은 이것이었다.

<BlogItem blog={blog} />

부모가 title이라는 이름으로 값을 넘기는 게 아니었다.

blog라는 이름의 props에 blog 객체 전체를 담아서 전달하고 있었다.

따라서 BlogItem이 실제로 받는 props의 구조는 다음과 같다.

{
    blog: {
        title: '...',
        content: '...'
    }
}

즉, title은 props 바로 아래에 있는 값이 아니었다.

props.title
// undefined

props.blog.title
// 실제 title 값

수정한 코드

const BlogItem = (props) => {
    const blog = props.blog;

    return (
        <Wrapper>
            <TitleText>
                {blog.title}
            </TitleText>
        </Wrapper>
    );
};

이렇게 수정하니 정상적으로 제목이 출력됐다.


오늘 가장 크게 이해한 부분

props에서 어떤 이름으로 값을 꺼내야 하는지는 자식 컴포넌트가 마음대로 결정하는 것이 아니다.

부모가 컴포넌트를 호출할 때 어떤 이름으로 전달했는지를 먼저 확인해야 한다.

<BlogItem blog={blog} />

이렇게 전달했다면 자식에서는

props.blog

로 접근해야 한다.

반대로 부모가 이렇게 전달했다면,

<BlogItem title={blog.title} />

자식에서는

props.title

로 접근할 수 있다.

결국 중요한 건 props의 데이터 구조를 먼저 확인하는 것이었다.


구조분해로 더 깔끔하게 작성하기

현재 코드는 이렇게 되어 있다.

const BlogItem = (props) => {
    const blog = props.blog;

    return (
        <TitleText>
            {blog.title}
        </TitleText>
    );
};

구조분해할당을 사용하면 한 단계 줄일 수 있다.

const BlogItem = ({ blog }) => {
    return (
        <TitleText>
            {blog.title}
        </TitleText>
    );
};

중첩 구조분해를 사용하면 title까지 바로 가져올 수도 있다.

const BlogItem = ({ blog: { title } }) => {
    return (
        <TitleText>
            {title}
        </TitleText>
    );
};

세 코드 모두 결과는 같다.

다만 아직 구조분해가 익숙하지 않은 상태라면 무조건 짧게 작성하기보다는, 데이터 구조가 눈에 보이는 방식으로 먼저 작성하는 게 더 이해하기 좋은 것 같다.

const blog = props.blog;

이렇게 직접 한 번 따라가 보는 과정이 오히려 이번에는 props 구조를 이해하는 데 도움이 됐다.


5. 다음 할 일

다음으로 잡아야 할 부분은 axiosuseEffect()다.

아직 아래 코드가 자연스럽게 읽히지는 않는다.

const loadData = async () => {
    await api
        .get('/blogs')
        .then((res) => {
            console.log('debug >>>> response:', res);

            if (res.status === 200) {
                setBlogs(res.data);
            }
        })
        .catch((error) => {
            console.log('debug >>>> response:', error);
        });
};

useEffect(() => {
    loadData();
}, []);

특히 아직 헷갈리는 건 이 부분이다.

  • async / await이 정확히 어떤 순서로 동작하는지
  • api.get('/blogs')가 무엇을 반환하는지
  • .then()await을 왜 같이 쓰고 있는지
  • setBlogs() 이후 React가 어떻게 다시 렌더링되는지
  • useEffect(() => {}, [])가 왜 처음 마운트될 때 한 번만 실행되는지
  • 잘못 작성했을 때 왜 무한 렌더링 또는 무한 API 호출이 발생하는지

내일 수업 시작 전에는 최소한 아래 흐름 정도는 자연스럽게 설명할 수 있게 만들자.

컴포넌트 렌더링 → useEffect 실행 → loadData() 호출 → API 요청 → 응답 수신 → setBlogs() → 상태 변경 → 재렌더링

오늘은 props 하나 때문에 꽤 오래 헤맸지만, 덕분에 React에서 데이터를 전달할 때는 단순히 변수 이름만 보는 게 아니라 “부모가 어떤 구조로 넘겼고, 자식이 어떤 구조로 받고 있는지”를 먼저 확인해야 한다는 걸 확실히 배웠다.

0개의 댓글