TIL/[TIL]

[TIL]React API 통신과 데이터 컴포넌트

namerong 2026. 4. 21. 15:19

1. 오늘 학습 주제

React에서 외부 API와 통신하는 방법과, 받아온 데이터를 화면에 구조적으로 출력하는 데이터 컴포넌트 패턴을 학습했다.

이전까지는 로컬 상태와 정적인 데이터를 다뤘다면, 오늘은 서버에서 데이터를 가져와 상태에 저장하고, 로딩과 에러를 처리하면서 화면에 반영하는 흐름을 연습했다.

  • fetch()의 기본 사용법
  • .then() 방식과 async/await 방식의 차이
  • axios를 사용하는 이유와 fetch와의 차이
  • useEffect로 마운트 시점에 API 호출하기
  • 로딩, 에러, 결과 상태를 나누어 관리하기
  • 부모 컴포넌트가 데이터를 관리하고 자식 컴포넌트가 화면을 렌더링하는 데이터 컴포넌트 구조

2. fetch 기본 문법

브라우저에서 API 요청을 보낼 때 가장 기본적으로 사용할 수 있는 함수가 fetch()이다.

fetch()는 Promise를 반환하며, 응답 본문은 바로 사용할 수 없고 json() 같은 메서드로 한 번 더 파싱해야 한다.

const promise = fetch('<https://jsonplaceholder.typicode.com/users>');
const response = await promise;
const responseJSON = await response.json();

정리

  • fetch()는 Promise를 반환한다.
  • 응답 객체는 Response 형태로 도착한다.
  • 실제 JSON 데이터는 response.json()으로 변환해야 사용할 수 있다.
  • response.json()도 비동기이므로 await가 필요하다.

3. Response 객체와 bodyUsed

fetch()의 응답 객체는 본문을 한 번만 읽을 수 있다.

예제에서는 response.bodyUsed를 통해 본문 사용 여부를 확인했다.

console.log(response.bodyUsed);
const data = await response.json();
console.log(response.bodyUsed);

정리

  • 응답 본문은 한 번만 꺼내 쓸 수 있다.
  • response.json()을 호출하면 본문이 소비된다.
  • 같은 응답에서 여러 번 json()을 호출할 수 없다.

4. fetch와 then 체이닝

fetch()는 Promise 기반이기 때문에 .then() 체이닝 방식으로도 처리할 수 있다.

fetch(API_URL)
    .then(response => {
        if (!response.ok) {
            throw new Error(`HTTP error ${response.status}`);
        }
        return response.json();
    })
    .then(data => {
        console.log(data);
    })
    .catch(error => {
        console.error('데이터를 가져오는데 실패했습니다:', error);
    });

정리

  • 첫 번째 .then()에서는 응답 객체를 받는다.
  • 두 번째 .then()에서는 파싱된 실제 데이터를 받는다.
  • .catch()로 전체 과정의 에러를 처리할 수 있다.
  • response.ok를 확인해 HTTP 에러를 직접 처리해야 한다.

5. fetch와 async/await

같은 작업을 async/await로 작성하면 흐름이 더 직관적으로 보인다.

async function fetchDataWithAsyncAwait() {
    try {
        const response = await fetch(API_URL);

        if (!response.ok) {
            throw new Error(`HTTP error: ${response.status}`);
        }

        const data = await response.json();
        console.log(data);
    } catch (error) {
        console.log(error);
    }
}

정리

  • async/await는 비동기 코드를 동기 코드처럼 읽기 쉽게 만든다.
  • try...catch로 에러 처리가 가능하다.
  • 로직이 길어질수록 .then()보다 가독성이 좋아지는 경우가 많다.

6. fetch와 axios 차이

오늘은 axios를 사용한 API 요청도 함께 확인했다.

axios는 별도 라이브러리지만, API 요청을 더 편하게 처리할 수 있다.

const response = await axios.get(API_URL);
console.log(response.data);

반면 fetch는 아래처럼 직접 JSON 파싱이 필요하다.

const response = await fetch(API_URL);
const data = await response.json();

정리

  • fetch는 브라우저 내장 API이다.
  • axios는 외부 라이브러리이다.
  • axios는 JSON을 자동으로 파싱해 준다.
  • axios의 실제 데이터는 response.data에 들어 있다.
  • fetch는 HTTP 에러를 자동 reject 하지 않기 때문에 response.ok 확인이 필요하다.

7. React에서 API 호출하기

React에서는 보통 컴포넌트가 처음 렌더링된 뒤 API를 호출하기 위해 useEffect()를 사용한다.

useEffect(() => {
    async function fetchEmojis() {
        try {
            const response = await axios.get('<https://api.github.com/emojis>');
            setEmojis(response.data);
        } catch (error) {
            console.error(error);
        } finally {
            setLoading(false);
        }
    }

    fetchEmojis();
}, []);

정리

  • useEffect(..., [])는 마운트 시 한 번 실행된다.
  • 초기 데이터 로딩에 자주 사용하는 패턴이다.
  • API 결과를 상태에 저장하면 화면이 자동으로 갱신된다.

8. 로딩 상태 관리

API 요청은 시간이 걸릴 수 있기 때문에, 데이터를 기다리는 동안 사용자에게 현재 상태를 알려줘야 한다.

const [loading, setLoading] = useState(true);
if (loading) return <p>로딩 중...</p>;

정리

  • API 요청 시작 전에는 loading을 true로 둔다.
  • 요청이 끝나면 false로 바꾼다.
  • 로딩 화면을 별도로 분기하면 사용자 경험이 좋아진다.

9. 에러 상태 관리

네트워크 문제나 응답 문제로 데이터 요청이 실패할 수 있다.

이때는 에러 상태를 따로 관리하는 것이 좋다.

const [error, setError] = useState(null);
.catch(error => {
    console.error('데이터를 불러오는 데 실패했습니다.', error);
    setError(error);
})
if (error) {
    return <p>오류가 발생했습니다: {error.message}</p>;
}

정리

  • 에러도 상태로 관리하면 UI에서 사용자에게 바로 알려줄 수 있다.
  • 콘솔 출력만으로 끝내지 않고 화면 분기를 함께 처리하는 것이 중요하다.
  • 로딩, 에러, 성공 상태를 나눠서 생각하는 습관이 필요하다.

10. Early Return 패턴

조건에 따라 화면을 빠르게 분기하는 패턴을 Early Return이라고 한다.

if (loading) {
    return <p>로딩 중...</p>;
}
if (error) {
    return <p>오류가 발생했습니다: {error.message}</p>;
}

정리

  • 조건이 명확할 때 코드를 읽기 쉽게 만든다.
  • 복잡한 삼항 연산자나 중첩 조건문을 줄일 수 있다.
  • 로딩과 에러처럼 먼저 처리해야 할 상태에 특히 잘 어울린다.

11. 데이터 컴포넌트 패턴

오늘 예제에서는 부모 컴포넌트가 데이터를 관리하고, 자식 컴포넌트가 화면을 출력하는 구조를 확인했다.

예를 들어 ItemList는 데이터를 가져오고 상태를 관리한다.

function ItemList() {
    const [users, setUsers] = useState([]);
    const [loading, setLoading] = useState(true);
    const [error, setError] = useState(null);
}

반면 Item 컴포넌트는 전달받은 사용자 한 명의 정보를 화면에 출력한다.

function Item({ user }) {
    return (
        <div className="item">
            <h4>{user.name}</h4>
            userId : {user.id} <br />
            phone : {user.phone} <br />
            company name: {user.company?.name}
        </div>
    );
}

정리

  • 부모 컴포넌트는 데이터, 로딩, 에러 상태를 관리한다.
  • 자식 컴포넌트는 props를 받아 화면만 담당한다.
  • 관심사를 분리하면 코드 구조가 더 명확해진다.

12. 검색 기능과 부모-자식 협업

GitHub Emoji 예제에서는 검색 입력은 자식 컴포넌트가 담당하고, 실제 검색 결과 상태는 부모가 관리했다.

function SearchBox({ onSearch }) {
    const [inputText, setInputText] = useState('');

    const handleSearchClick = () => {
        onSearch(inputText);
    };
}

부모는 전체 이모지 데이터를 들고 있고, 검색어를 받아 결과를 판별한다.

const handleSearch = (emojiName) => {
    if (emojis && emojis[emojiName]) {
        setImgUrl(emojis[emojiName]);
    } else {
        alert('이모지를 찾을 수 없습니다.');
        setImgUrl(null);
    }
};

정리

  • 입력은 자식에서 받을 수 있다.
  • 실제 데이터 판단과 상태 변경은 부모가 담당할 수 있다.
  • 부모가 데이터를 소유하고 자식은 행동을 전달하는 구조는 매우 자주 쓰인다.

13. 조건부 렌더링으로 결과 표시하기

검색 결과 컴포넌트에서는 상태에 따라 화면을 다르게 보여주었다.

function ImageBox({ imgUrl, loading }) {
    if (loading) return <p>로딩 중...</p>;
    if (!imgUrl) return <p>이모지를 검색해주세요</p>;

    return (
        <div style={{ marginTop: '20px' }}>
            <img src={imgUrl} alt="emoji" style={{ width: '100px' }} />
        </div>
    );
}

정리

  • 조건부 렌더링은 상태에 따라 다른 화면을 보여주는 기본 패턴이다.
  • 로딩 상태, 빈 결과 상태, 실제 결과 상태를 분리해서 생각할 수 있다.
  • 데이터를 다루는 컴포넌트에서 매우 자주 사용된다.

14. 옵셔널 체이닝

사용자 데이터 안에 중첩된 객체가 있을 때, 값이 없는 상황에서도 에러 없이 접근하려면 옵셔널 체이닝을 사용할 수 있다.

company name: {user.company?.name}

정리

  • ?.는 앞의 값이 null 또는 undefined일 때 에러를 방지한다.
  • API 데이터처럼 구조가 항상 같다고 확신할 수 없을 때 유용하다.
  • 중첩 객체 접근 시 안전성을 높여 준다.

15. 오늘 배운 핵심 정리

  1. fetch()는 Promise를 반환하며, 응답 본문은 response.json()으로 파싱해야 한다.
  2. response.json()도 비동기이며, 응답 본문은 한 번만 읽을 수 있다.
  3. fetch는 .then() 체이닝 방식과 async/await 방식 둘 다 사용할 수 있다.
  4. fetch는 HTTP 에러를 자동으로 reject 하지 않기 때문에 response.ok를 직접 확인해야 한다.
  5. axios는 JSON을 자동 파싱하고 response.data로 결과를 꺼낼 수 있어 더 편리하다.
  6. React에서는 useEffect(..., [])를 사용해 마운트 시점에 API를 호출하는 패턴을 자주 사용한다.
  7. API 통신에서는 loading, error, data 상태를 나누어 관리하는 것이 중요하다.
  8. Early Return 패턴은 로딩과 에러 처리를 깔끔하게 분기하는 데 유용하다.
  9. 부모 컴포넌트는 데이터를 관리하고 자식 컴포넌트는 화면을 렌더링하는 데이터 컴포넌트 구조가 자주 사용된다.
  10. 검색 기능에서는 자식이 입력을 받고, 부모가 데이터를 기준으로 결과를 판별하는 구조를 만들 수 있다.
  11. 조건부 렌더링을 사용하면 상태에 따라 다른 UI를 자연스럽게 보여줄 수 있다.
  12. 옵셔널 체이닝은 API 데이터 접근 시 안전성을 높여 준다.