본문 바로가기

React

[React] 📝async/await + useEffect 내 순차 실행하기(비동기 제어 처리)

0. 배경 설명

제목과 관련된 이야기를 시작하기 전에 간단하게 배경 설명을 해야겠다.

 

거의 마무리 단계에 있는 프로젝트인 '잼시네마(영화 정보 사이트)'의 뉴스 탭을 개발 중에

내가 서버 측 코드 newsRouter.ts 에 만들어놓은 두 가지 API가 있었다.

 

첫 번째 API는 fetch-and-store 이라는 이름(주소?)의 API이다.

외부 API인 딥서치 뉴스 API를 사용하여 우리가 원하는 영화라는 키워드와 관련된 뉴스 기사들의 정보를 받아오고

그 데이터들을 정제하여 우리 프로젝트의 DB에 저장하는 API로 커스텀 한 것이다.

 

두 번째 API는 list라고 이름 지은 API이다.

이 API의 역할은 DB에 저장된 뉴스 데이터들을 날짜 내리참순으로 가져와서 프론트(클라이언트) 측으로 보내준다.

 

(해당 코드 참고)

newsRouter.get('/fetch-and-store', async (req, res) => {
  const url = `https://api-v2.deepsearch.com/v1/articles?keyword=영화&page_size=30&api_key=${news_api_key}`;

  try {
    const response = await fetch(`${url}`, {
      method: 'GET',
    });

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

    const data = await response.json();
    const articles = data.data; // 뉴스 데이터 배열

    if (!articles || articles.length === 0) {
      return res.status(400).json({ error: 'No articles found' });
    }

    // 중복 체크를 위한 ID 리스트 가져오기
    const existingIds = new Set();
    const existingRows = await new Promise<any[]>((resolve, reject) => {
      db.query('SELECT id FROM news', [], (err, result) => {
        if (err) reject(err);
        resolve(result);
      });
    });
    existingRows.forEach((row) => existingIds.add(row.id));

    // 새로운 뉴스만 삽입
    const insertPromises = articles
      .filter((article: any) => !existingIds.has(article.id)) // 중복 제거
      .map((article: any) => {
        return new Promise<void>((resolve, reject) => {
          const sql = `
            INSERT INTO news (id, title, summary, content_url, image_url, thumbnail_url, published_at, publisher)
            VALUES (?, ?, ?, ?, ?, ?, ?, ?)`;

          db.query(
            sql,
            [
              article.id,
              article.title,
              article.summary,
              article.content_url,
              article.image_url,
              article.thumbnail_url,
              article.published_at,
              article.publisher,
            ],
            (err) => {
              if (err) reject(err);
              resolve();
            }
          );
        });
      });

    await Promise.all(insertPromises); // 모든 삽입 작업 완료

    res.json({
      message: `${insertPromises.length} articles stored successfully`,
    });
  } catch (error) {
    console.error('Error fetching or storing data:', error);
    res.status(500).json({ error: 'Failed to fetch and store news' });
  }
});

// 2. DB에서 뉴스 리스트 가져오기
newsRouter.get('/list', (req, res) => {
  db.query(
    'SELECT * FROM news ORDER BY published_at DESC',
    [],
    (err, result) => {
      if (err) {
        res.status(500).json('뉴스 리스트 불러오기 실패');
        throw err;
      } else {
        res.json(result);
      }
    }
  );
});

 

 


 

 

이제 프론트 단에서 이 API를 호출하여 뉴스 탭을 클릭했을 때 페이지 이동과 함께 화면에 각 뉴스 기사들을 무한 스크롤

방식으로 주루룩 나열하려고 했다. (무한 스크롤 방식에 대해서는 다음 포스트에서 다루어 보겠다.)

 

 

처음에는 단순히 코드의 작동만을 목표로 생각 없이 코딩했던지라 아래와 같이 useEffect를 2번 사용하여

fetch-and-store 한 번, list 한 번 작동을 시켰다.

 interface INewsData {
  id: string;
  publisher: string;
  published_at: string;
  title: string;
  summary: string;
  image_url: string;
  content_url: string;
  thumbnail_url: string;
}

 const [newsData, setNewsData] = useState<INewsData[]>([]);
  
  useEffect(() => {
    axios
      .get('http://localhost:8001/news/fetch-and-store')
      .then((res) => {
        console.log(res);
      })
      .catch((err) => console.log(err));
  }, []);

  useEffect(() => {
    axios
      .get('http://localhost:8001/news/list')
      .then((res) => {
        console.log(res);
        setNewsData(res.data);
      })
      .catch((err) => console.log(err));
  }, []);

 

 


 

1. 문제 상황

당연히 작동이 제대로 될 리가 없었고 프론트단에서 state 변수로 만들어놓았던 newsData 배열이 undefined인 채로

map 함수가 작동을 하니 오류가 발생했다. 이 오류는 초기 값을 [] 와 같이 빈 배열로 만들어줌으로써 해결은 하였지만

뉴스 기사가 DP 되지 않는 것은 마찬가지였다.

 

일단 전체적인 틀을 잡고 러프하게 코드를 짜고 오류나 문제를 하나씩 해결해가며 개발을 하는 스타일이기에

이 글을 보는 분들은 나를 좀 바보 같다고 생각하실 거 같다.

 

콘솔을 찍어보니 두 API 자체는 동작이 정상적으로 이루어졌다.

 

fetch-and-store이 작동하여 딥서치 API에서 영화 관련 뉴스를 잘 가져와 DB(아래는 본 프로젝트 DB의 news 테이블과 저장된 예시 데이터들이다)에 잘 저장을 하였고

 

list 또한 state변수 newsData에 저장이 잘 되었다.

 

 


 

1-1. 문제 원인


근데 왜 빈 배열로 콘솔에 찍히고 map함수도 제대로 동작하지 않았을까?

 

답은 간단했다. 비동기 제어 처리를 해주지 않았기 때문에 내 의도와 달리 순차 실행이 되지 않았기 때문이다.

 

여기서 내가 하고자 했던 의도의 순차 실행은

1. 먼저 fetch-and-store가 실행되어 DB에 뉴스 데이터들을 저장함

2. 그 다음 list가 실행되어 DB에 있는 뉴스 데이터들을 가져와 state변수(배열) newsData에 저장함 

3. 그 다음 그 newsData를 map 함수를 이용해 화면에 나열함

이다.

 

그렇지만 코드는 비동기로 짜놓고 따로 제어 처리를 해주지 않았기 때문에 

fetch-and-store가 끝나기도 전에 list 요청이 실행되어 DB에 아직 뉴스 데이터가 저장되지 않은 상태에서

list 요청이 실행되므로 빈 배열 반환되어 결국 화면에는 뉴스 기사가 보이지 않은 것이다.

 

그러면 해결방법으로는 동기처리를 제대로 해주던지

아니면 비동기의 제어 처리를 함으로써 실행 순서를 컨트롤해야 한다.

 

 


 

1-2. 1차 문제 해결(의존성 배열)

 

그래서 일단 API 실행 코드를 useEffect 내에 작성했기에 의존성 배열을 이용하여 제어 처리를 했다.

check라는 state 변수를 만들고 초기값을 0으로 지정했다.

 

첫 번째 fetch-and-store가  포함되어 있는 useEffect가 동작할 때  check 변수를 1로 변경하고

두 번째 list 주관하는 useEffect의 의존성 배열에 check를 넣음으로써 fetch-and-store가 동작한 후

list가 동작하여 통신할 수 있도록 했다.

 

코드는 아래와 같다.

 const [newsData, setNewsData] = useState<INewsData[]>([]);
 const [check, setCheck] = useState(0);	// 1️⃣ 비동기 제어처리를 위한 의존성 state변수 check
	
  useEffect(() => {
    axios
      .get('http://localhost:8001/news/fetch-and-store')
      .then((res) => {
        console.log(res);
        setCheck(1); // 2️⃣ fetch-and-store가 정상적으로 통신 되었다면 check 값을 변경함
      })
      .catch((err) => console.log(err));
  }, []);

  useEffect(() => {
    axios
      .get('http://localhost:8001/news/list')
      .then((res) => {
        console.log(res);
        setNewsData(res.data);
      })
      .catch((err) => console.log(err));
  }, [check]);	// ✅ 의존서 배열 부분에 check state 변수를 넣어줌으로써 check값에 변동이 
		// 생겼을 경우에만(이 경우는 fetch-and-store가 정상 작동 된 후)
		// list 가 통신되도록 함

 

그 결과, 원했던 대로 뉴스 페이지가 기능을 수행했다.

하지만 이는 결과만 정상적으로 되었을 뿐 과정은 야매로 한 것이었기에 다른 제어처리 방법을 알아보기로 했다.

 

 

 


 

2. 2차 문제 해결(async/await)

 

서치 결과 크게 3가지 방법으로 나누어 볼 수 있겠다

 

1. 콜백(Callback) 함수 기법

2. 프로미스(Promise), then. 체이닝 기법

3. async/await ✅

 

이렇게 3가지로 나눠볼 수 있겠다.

 

이 3가지 중 이번 포스트에서는 내가 사용한 async/await 을 다뤄보도록 하겠다.

다른 2가지 방법은 기회가 된다면 나중에 포스팅 해야겠다.

 

일단 변경된 코드는 다음과 같다.

const [newsData, setNewsData] = useState<INewsData[]>([]);

useEffect(() => {
  const fetchNews = async () => {
    try {
      // 1️⃣ 뉴스 데이터 가져와서 DB 저장
      await axios.get('http://localhost:8001/news/fetch-and-store');

      // 2️⃣ 저장된 뉴스 데이터를 다시 가져오기
      const response = await axios.get('http://localhost:8001/news/list');
      console.log(response);
      setNewsData(response.data);
    } catch (error) {
      console.error('Error fetching news:', error);
    }
  };

  fetchNews();
}, []); // ✅ 의존성 배열이 빈 배열이므로 컴포넌트 마운트 시 한 번만 실행

 

코드를 하나하나 분석하며 해설을 하겠습니다.

 

1. const [newsData, setNewsData] = useState<INewsData[]>([]);

✅ useState를 사용하여 newsData라는 상태(state)를 선언.
✅ 기본값은 빈 배열 [], 즉 뉴스 데이터가 들어오기 전 상태.
✅ setNewsData로 newsData 상태 업데이트 가능.

 

2. useEffect 내부 코드

✅ useEffect는 컴포넌트가 처음 렌더링 될 때 한 번 실행됨.
✅ 비동기 함수를 fetchNews로 정의한 후, 즉시 실행.

 

코드 실행 흐름을 살펴보면 

 

첫 번째로,

이 뉴스 페이지 리스트 컴포넌트가 마운트 되며 useEffect가 실행되고

그 안에 있던 fetchNews가 실행됩니다.

 

두 번째로,

뉴스데이터를 가져와서 DB에 저장합니다.

1️⃣ await axios.get('http://localhost:8001/news/fetch-and-store');

 

  • 백엔드 API /news/fetch-and-store를 호출해서 외부 뉴스 API에서 데이터를 가져와 DB에 저장해.
  • await를 사용했기 때문에 이 작업이 끝날 때까지 기다린 후 다음 단계로 넘어감.

세 번째로,

DB에 저장된 뉴스 데이터를 다시 가져옵니다.

2️⃣ const response = await axios.get('http://localhost:8001/news/list');

 

  • 뉴스 데이터를 저장한 후, news/list API를 호출하여 DB에 저장된 최신 뉴스 리스트를 가져옴.
  • await로 요청이 완료될 때까지 기다린 후 response.data를 받아옴.

마지막 네 번째로,

  • setNewsData를 호출해서 newsData 상태를 업데이트.
  • 이로 인해 컴포넌트가 리렌더링되면서 최신 뉴스 데이터가 화면에 반영됨.

 

 


 

3. 궁금한 점

 

이러고 난 후 의문이 하나 들었다.

fetchNews 함수를 굳이 만들 필요 없이 그냥 async를 바로 선언하면 안 될까 하는 것이었다.

 

구글링을 통해 알아본 결과 이유는 이러했다.

useEffect는 콜백함수가 아무것도 반환하지 않거나 clean-up function을 반환해야 한다.

그런데 콜백함수 내에서 async/await 구문을 직접적으로 사용하면 자동으로 함수를 비동기 함수로 전환하고

Promise를 반환하기 때문에 useEffect hook의 동작과 충돌하여 예기치 않은 문제가 발생할 수 있기에

내부에 별도의 함수, 여기선  fetchNews를 정의하고 즉시 호출하는 방법을 사용한다고 합니다.

(출처 : https://velog.io/@do_dam/useEffect-%EB%82%B4%EC%97%90%EC%84%9C-asyncawait-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0

 

 


 

4. 결론

 

결론을 맺고자 합니다.

여러 방식 중에서 async/await 를 채택한 이유는 다음과 같습니다.

 

 

💡 이 방식을 채택한 이유(장점)

  1. 불필요한 상태(check) 제거 → 더 직관적이고 간결한 코드
  2. 첫 번째 요청이 끝난 후에 두 번째 요청 실행 (await 사용)
  3. 에러 처리 개선 → try/catch로 오류 발생 시 로그 출력

이러한 장점들을 가지고 있기에 위에서 말했던 방법 중

이러한 비동기 흐름을 보장하는 제어 처리를 한 것입니다.

 

 

비동기 흐름을 보장해야 하는 이유

  1. fetch-and-store: 외부 API에서 데이터를 받아와 DB에 저장하는 작업 → 시간이 걸릴 수 있음.
  2. list: DB에서 뉴스 데이터를 불러오는 작업 → ①이 끝나기 전에 실행되면, 데이터가 없을 가능성이 큼.

 

그래서 await을 사용하거나, then을 이용해서 첫 번째 요청이 끝난 후 두 번째 요청이 실행되도록 만들어야  함.

 

 

 

+ 추가로 이러한 API 요청과 통신 함수들은 시간이 꽤나 걸릴 수 있기 때문에 로딩 상태를 추가해서 UI에 반영해주면 사용자 경험이 더 좋아지는 효과를 얻을 수 있다고 합니다.

 

+ 또 에러 핸들링 측면에서 axios.get 요청이 실패했을 때 사용자에게 메시지를 표시해주면 UX 개선이 이루어질 것입니다.

 

+ 그리고 저는 이 뉴스 리스트 페이지에서 각 뉴스 기사들을 무한 스크롤 방식으로 보여줄 것이기에 여기에 추가적인 코드 추가를 하였습니다.