상권's

TIL 30 (React 에러 및 서버 구현 에러) (2021.11.05~2021.11.06) 본문

~2022 작성 글/TIL

TIL 30 (React 에러 및 서버 구현 에러) (2021.11.05~2021.11.06)

라마치 2021. 11. 6. 13:10

클라이언트 앱을 통해서 영화 정보를 조회하는 서버를 구현하는 과정에서 발생했던 에러입니다.

 

REST Api를 구현하는 과정에서, movies/:id 값이 들어오면 해당 파라미터와 동일한 정보를 respone해야 했습니다.

파라미터 값이 들어가진 않는 조회 기능은 손쉽게 구현할 수 있었지만,

파라미터 값이 들어가는 경우 조회 기능을 아래와 같이 구현했을 때 작동하지 않았습니다.

app.get('/movies', (req, res) => {
  return res.status(200).send(movies);
});

app.get('/movies/:id', (req, res) => {
  const filtered = movies.filter((movie) => {
    return req.params.id === movie.id
  })
  if(filtered.length !== 0) {
    return res.status(200).json(filtered[0]);
  }
});

에러부분을 확인하기 위해서 먼저 movies의 id가 무엇인지 출력해봤고, 이 경우에는 동일한 숫자 임을 확인했습니다.

동일한 숫자인데 조회가 안되는 이유를 찾기 위해서 response한 정보를 확인해보니 해당 정보가 JSON 객체였습니다.

그래서 type을 직접 출력해보니 아래와 같이 string이 나왔습니다.

app.get('/movies/:id', (req, res) => {
  console.log(req.params.id)      //8462
  console.log(movies[0]["id"])    //8462
});

app.get('/movies/:id', (req, res) => {
  console.log(typeof req.params.id)   //number
  console.log(typeof movies[0]["id"]) //string
});

JSON.stringfy한 movie.id 값을 req.params.id가 동일한 값을 찾기 위해 filter를 사용했고, 아래와 같이 error 처리를 해주었습니다. 

  const filtered = movies.filter((movie) => {
    return req.params.id === JSON.stringify(movie.id)
  })
  if(filtered.length !== 0) {
    return res.status(200).json(filtered[0]);
  }
  else {
    return res.status(404).json('Not Found');
  }

    Consider adding an error boundary to your tree to customize error handling behavior.
    Visit https://reactjs.org/link/error-boundaries to learn more about error boundaries.

과제를 진행하는 중에 fetch하는 과정에서 발생했던 에러입니다.

이 에러에서 나오는 Error Boundaries에 대해서 알아보겠습니다.

출처

에러 경계(Error Boundaries)의 소개
UI의 일부분에 존재하는 자바스크립트 에러가 전체 애플리케이션을 중단시켜서는 안 됩니다. React 사용자들이 겪는 이 문제를 해결하기 위해 React 16에서는 에러 경계(“error boundary”)라는 새로운 개념이 도입되었습니다.
에러 경계는 하위 컴포넌트 트리의 어디에서든 자바스크립트 에러를 기록하며 깨진 컴포넌트 트리 대신 폴백 UI를 보여주는 React 컴포넌트입니다. 에러 경계는 렌더링 도중 생명주기 메서드 및 그 아래에 있는 전체 트리에서 에러를 잡아냅니다.

Note
에러 경계는 다음과 같은 에러는 포착하지 않습니다.
이벤트 핸들러 (더 알아보기)
비동기적 코드 (예: setTimeout 혹은 requestAnimationFrame 콜백)
서버 사이드 렌더링
자식에서가 아닌 에러 경계 자체에서 발생하는 에러

 

컴포넌트 스택 추적
React 16은 애플리케이션이 실수로 에러를 집어삼킨 경우에도 개발 과정에서 렌더링하는 동안 발생한 모든 에러를 콘솔에 출력합니다. 에러 메시지 및 자바스크립트 스택과 더불어 React 16은 컴포넌트 스택 추적 또한 제공합니다. 이제 정확히 컴포넌트 트리의 어느 부분에서 에러가 발생했는지 확인할 수 있게 되었습니다.
또한 컴포넌트 스택 추적 내에서 파일 이름과 줄 번호도 확인할 수 있습니다. 이는 Create React App 프로젝트 내에서 기본적으로 동작합니다.

컴포넌트 스택 추적

공식 홈페이지에서는 class 컴포넌트에서의 해결 방법을 제공하고 있습니다. 자바스크립트 에러에 대해서 알려주는 것으로 확인됩니다. 구현한 코드에서 문제가 발생한 것으로 사료되며, 해당 문제가 발생하지 않을 수 있도록 코드를 구현하는 것이 맞지 않을까.. 라는 생각을 합니다.

=> 추가적으로 학습하게 된다면 블로깅하도록 하겠습니다.


Warning: Can't perform a React state update on an unmounted component. 
This is a no-op, but it indicates a memory leak in your application. 
To fix, cancel all subscriptions and asynchronous tasks in %s.%s a useEffect cleanup function 
at App (/home/sangkwon/im-ha-section-2/client/src/App.js:13:37)

 

컴퓨터 과학에서 NOP 또는 NOOP은 어셈블리어의 명령, 프로그래밍 언어의 문, 컴퓨터 프로토콜 명령의 하나로, 아무 일도 하지 않는다.
출처 위키백과
언마운티드 컴포넌트에서는 React state 업데이트를 수행할 수 없다. 아무런 작업이 진행되지 않지만, 애플리케이션에서 메모리 누수가 발생한다.

뒷 부분은 구독이랑 비동기 작업을 취소하라는 말인 거 같은데.. 무슨 뜻인지 이해를 못해서 알아보니 useEffect의 cleanup function을 이용하면 된다고 합니다.

 

정리(clean-up)를 이용하는 Effects 출처

위에서 정리(clean-up)가 필요하지 않은 side effect를 보았지만, 정리(clean-up)가 필요한 effect도 있습니다. 외부 데이터에 구독(subscription)을 설정해야 하는 경우를 생각해보겠습니다. 이런 경우에 메모리 누수가 발생하지 않도록 정리(clean-up)하는 것은 매우 중요합니다. class와 Hook을 사용하는 두 경우를 비교해보겠습니다.

 

Hook을 이용하는 예시

이제 이 컴포넌트를 Hook을 이용하여 구현해봅시다.

정리(clean-up)의 실행을 위해 별개의 effect가 필요하다고 생각할 수도 있습니다. 하지만 구독(subscription)의 추가와 제거를 위한 코드는 결합도가 높기 때문에 useEffect는 이를 함께 다루도록 고안되었습니다. effect가 함수를 반환하면 React는 그 함수를 정리가 필요한 때에 실행시킬 것입니다.

import React, { useState, useEffect } from 'react';

function FriendStatus(props) {
  const [isOnline, setIsOnline] = useState(null);
--------------------------------------------------------------------------------
  useEffect(() => {
    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }
    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    // effect 이후에 어떻게 정리(clean-up)할 것인지 표시합니다.
    return function cleanup() {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });
--------------------------------------------------------------------------------
  if (isOnline === null) {
    return 'Loading...';
  }
  return isOnline ? 'Online' : 'Offline';
}

effect에서 함수를 반환하는 이유는 무엇일까요? 이는 effect를 위한 추가적인 정리(clean-up) 메커니즘입니다. 모든 effect는 정리를 위한 함수를 반환할 수 있습니다. 이 점이 구독(subscription)의 추가와 제거를 위한 로직을 가까이 묶어둘 수 있게 합니다. 구독(subscription)의 추가와 제거가 모두 하나의 effect를 구성하는 것입니다.

 

React가 effect를 정리(clean-up)하는 시점은 정확히 언제일까요? React는 컴포넌트가 마운트 해제되는 때에 정리(clean-up)를 실행합니다. 하지만 위의 예시에서 보았듯이 effect는 한번이 아니라 렌더링이 실행되는 때마다 실행됩니다. React가 다음 차례의 effect를 실행하기 전에 이전의 렌더링에서 파생된 effect 또한 정리하는 이유가 바로 이 때문입니다.

 

cleanup함수를 useEffect내부에서 사용을 하면 되는 것으로 확인이 되었습니다. 추가적인 자료를 통해서 아래와 같이 코드를 구현할 수 있었습니다.

  useEffect(()=> {
    let istrue = true;
    getMovies().then(res => {
      if (istrue) setMovieList(res);
    });
    return () => {
      istrue = false;
    };
  }, [])

그리고 또 직면한 문제는 위와 같습니다. ajax 호출 횟수가 3회가 나왔습니다. 이 문제는 해결하긴 했지만, 왜 기존의 코드가 3회이고, fetch하는 함수를 변수에 저장해서 이용해면 1회로 줄어드는 지에 대해서는 이해를 하질 못했습니다.

이 문제 또한 error boundaries와 같이 추가적인 학습을 해서 이해가 된다면 블로깅하도록 하겠습니다.

 


오늘은 SECTION 2 HA를 진행하면서 나왔던 에러코드에 대해서 학습해봤습니다. 실력이 부족해서 발생하는 에러도 있고, 학습량이 적어서 발생하는 에러도 맞이해봤습니다. 오늘처럼 에러 리뷰를 통해서 프로젝트를 진행할 때나 취업 후 업무를 할 때 에러 처리를 능숙하게 할 수 있도록 노력해야겠습니다.

 

app.get('/movies', (req, res) => {
  let limit = req.query.limit;
  let page = req.query.page;
  let gerne = req.query.gerne;
  let isString = Number(limit)

  if ( limit === undefined && page === undefined && req.query.genre !== undefined ) {
    let filterByGenre = movies.filter((movie) => {
      return movie.genres.includes(req.query.genre)
    })
    return res.status(200).send(filterByGenre);
  }
  if ( limit !== undefined && page !== undefined && req.query.genre !== undefined ) {
    let filterByGenre = movies.filter((movie) => {
      return movie.genres.includes(req.query.genre)
    })
    if (Number(page) === 1) {
      return res.status(200).send(filterByGenre.slice(0, limit))
    }
    else {
      let startPaged = Number(limit) * (Number(page) - 1)
      let endPaged = Number(limit) * Number(page)
      return res.status(200).send(filterByGenre.slice(startPaged, endPaged))
    }
  }
  if ( limit === undefined && page === undefined ) {
    return res.status(200).send(movies);
  }
  if ( limit === undefined && page !== undefined ) {
    return res.status(200).send(movies);
  }
  if ( limit !== undefined && page === undefined ) {
    if ( String(isString) === 'NaN' ) {
      return res.status(400).json('Not Found')
    }
    else {
      return res.status(200).send(movies.slice(0, limit))
    }
  }
  if ( limit !== undefined && page !== undefined && req.query.genre === undefined ) {
    if (Number(page) === 1) {
      return res.status(200).send(movies.slice(0, limit))
    }
    else {
      let startPaged = Number(limit) * (Number(page) - 1)
      let endPaged = Number(limit) * Number(page)
      return res.status(200).send(movies.slice(startPaged, endPaged))
    }
  }
  else {
    return res.status(400);
  }
});

이번에 구현했던 서버 코드입니다. 아직 많이 부족해서, 코드가 난잡한데 꾸준히 시간을 투자해서 해당 코드를 깔끔하게 만들어보도록 하겠습니다.

Comments