에러 해결 - 02. TypeError: articles.map is not a function

이유승·2023년 7월 11일
0

에러 해결

목록 보기
2/25

배열 형식의 데이터와 점 표기법으로 호출한 map 함수를 사용하였을 때, 위와 같은 에러가 발생되었다.



1. map 함수는 어떻게 사용할 수 있는가?

근본으로 잠시 돌아와서, 나는 단지 배열 형식의 데이터를 만들었을 뿐인데 여기서 바로 map 함수를 호출할 수 있다. 도대체 이 map 함수는 어디서 온 것인가?

Prototype - 프로토타입 객체!

쉽게 말해서 내가 배열 형식의 데이터를 만들 경우, new 연산자를 사용하지 않았더라도 이 데이터는 Array 객체를 바탕으로 만들어진 '인스턴스'에 해당한다.

그리고 부모 Array 객체가 가지고 있던 프로퍼티 혹은 메소드들은 프로토타입 객체에 의해 자식 객체로 전달되게 된다. 그렇기에 우리는 단지 배열 형식의 데이터를 만들기만 해도 배열 전용 메소드들을 사용할 수 있는 것이다.



2. map is not a function!

map은 함수가 아니다. 정상적인 배열 데이터에서 map 함수를 사용하지 못할 리가 없다. 그렇다면 이 에러의 원인은 단순하다. 내가 배열이라고 생각하고 있는 데이터가 사실 배열이 아니라는 것이다.

사실 이 에러는 내가 여러가지 프로젝트를 진행하면서 정말 많이 만났던 에러였다. map 함수를 사용한 배열 렌더링을 구현할 것이고, 따라서 데이터는 반드시 배열이여야 한다. 나는 분명히 이 데이터가 배열 형식을 유지하도록 구현했다고 생각했는데 에러가 발생한다!



3. 무엇이 원인인가?

위에서도 언급했듯이 이 에러의 원인은 데이터 형식이 배열이 아닐 때 발생한다. 단순하지만, 개발에 대해 배워가던 시기에는 어처구니 없는 실수를 저질러서 에러가 발생했다.

1. 데이터의 초기값 설정 실수.

	const [data, setData] = useState();

데이터를 받을 변수를 만들었고, 여기에 데이터를 받아올 함수도 만들었다. 그런데 리액트에서는 일단 처음 한번 렌더링을 실행하고, 데이터가 변화하면 리렌더링을 실행하는데 초기값이 배열이 아니었다. 그렇다보니 최초 렌더링이 실행되는 시점에서 데이터 형식이 배열이 아니었고, 에러가 발생하였다.

2. 배열과 배열이 아닌 데이터를 하나의 변수에서 다루었을 때.

  const { getComments, addComments, responseData } = useFirestoreComt('comment');

마이 블로그 프로젝트 진행 도중, 현재 DB에 저장된 댓글 데이터를 가져오고 / 새로운 댓글 데이터를 저장하는 기능을 구현하였다. Context API로 state를 관리하면서 저장되어 있는 댓글 데이터와 새롭게 작성한 댓글 데이터에 대한 내용을 모두 Store 내부의 동일한 state에서 관리하였는데, 이것이 문제의 원인이 되고 말았다.

const storeReducer = (state, action) => {
    switch (action.type) {
        case 'isPending':
            return { isPending: true, document: null, success: false, error: null }
        case 'addComment':
            return { isPending: false, document: action.payload, success: true, error: null }
        case 'getComment':
            return { isPending: false, document: action.payload, success: true, error: null }
        case 'error':
            return { isPending: false, document: null, success: false, error: action.payload }
        default:
            return state
    };
};        

저장되어 있는 댓글 데이터는 배열 데이터였지만, 새롭게 작성한 댓글 데이터는 객체였다.

   {responseData.document?.map((item) => (
  		<div className={styles.recorditemcommenttext} key={item.id}>
              <p>{item.comments}</p>
              <p className={styles.commentsinfo}>{item.writer}, {item.createdTime}</p>
        </div>
  ))}

페이지가 첫 렌더링 될 때는 responseData에 배열 데이터가 들어와서 map 함수가 정상 동작하지만, 댓글을 작성하는 순간 responseData에 객체 데이터가 들어옴으로써 에러가 발생하고 있던 것이다.



4. 의외로 간단하지가 않다..?

이 에러는 원인은 단순한데, 내 개발 실력의 문제로 갈 수록 해결이 복잡한 경우가 나타나게 된다. 나중에 진행했던 리액트와 리덕스를 기반으로 만든 쇼핑몰에서는 전역 state 관리에 Redux를 적용했는데, 여기서도 여러가지 이유로 같은 에러가 발생했었다.

1. 데이터의 초기값 설정 실수.

const initialState = {
    flagValue: {
        isError: false,
        isLoading: false,
        isRendering: false,
        isPointEnough: true,
    },
    processInfo: {
        processCode: '',
        processMessage: '',
        processData1: {},
        processData2: [],
    },
    purchaseData: {
        purchaseList: [],
        totalQuantity: 0,
        totalAmount: 0,
    },
    basketData: {},
    reviewData: []
 };

리덕스 스토어에서 State의 초기값을 설정했을 때, 빈 배열을 설정하는 것을 까먹었고. 쓰임새를 다한 State를 초기값으로 돌리는 기능을 구현하자 배열이 아닌 데이터에서 map 함수를 호출해 버렸다. 당연히 에러가 발생했다..

2. 배열과 배열이 아닌 데이터를 하나의 변수에서 다루었을 때.

이 경우는 내 리덕스와 파이어스토어에 관련된 지식과 사용 경험이 부족하여 발생한 문제였다. 프로젝트의 백엔드는 DB와 배포까지 한꺼번에 하기 위해서 구글 파이어스토어를 사용했는데, 가령 상품 데이터를 화면에 출력하는 기능을 구현할 때 쇼핑몰의 물건 데이터를 DB에서 가져와서 리덕스 Store에 한번 저장한 다음 컴포넌트에서 출력하는 구조로 기능을 구현하였었다.

문제의 핵심은 하나의 state에서 배열과 객체가 오락가락하는 어설픈 구조였다. 하나의 state에서는 하나의 타입만 사용하게 하던지, 그게 아니면 배열과 객체의 변화 타이밍을 정확하게 알고 있어서 문제가 없도록 잘 구현을 해야했는데 불행하게도 나는 어느 쪽도 아니었다. Store 내부에 하나의 역할만을 하는 여러 개의 state보다는 하나의 state에서 여러가지 역할을 할 수 있게 만들어야 한다고 단정지은 다음, 프로젝트 기능의 실행 구조를 거기에 맞춰 구현한 것이 모든 것의 원인이 된 것이다.

[] -> {[{}]} -> [{}] -> {} -> ????

여러 기능을 구현해 갈 수록 state의 변화 과정이 매우 변화무쌍해지기 시작했다...

객체 배열의 데이터가 필요한데 객체 내부의 객체 배열, 그냥 객체, 객체 내부의 객체 등. 데이터를 제어하는 라인이 완전히 파탄나버려서 에러가 잡히지가 않는 지경에 이르러버렸다. 문제의 심각성을 알아차린 시점에서 나는 Store 구조를 고치면서 지금까지 구현한 것들 전부를 수정하던지 Redux 개발자 도구 등을 이용해서 프로젝트 내부에서의 데이터 흐름을 파악하여 문제가 되는 지점마다 데이터 형식이 올바르게 수정되도록 고치던가 해야했다. 그리고 나는 후자를 선택하였다.

const result = [];
let data = {};
allDocumentSnapshots.forEach((doc) => {
    data = Object.assign(doc.data());
    data.recordDate = dateFormat(doc.data().recordDate.toDate());
    result.push(data);
});
returnData.processData2 = result;

수정 예시)

여기서는 파이어스토어에 저장된 timestamp 형식의 데이터가 문제가 되었다. 파이어스토어에서는 정상적인 날짜 데이터로 출력되지만 이걸 자바스크립트로 바로 가져오면 화면에 출력할 수가 없었고, 이를 처리하기 위해 timestamp 형식의 데이터를 변환해주는 dateFormat 함수를 구현하였다.

그런데 DB에 저장된 문서 하나하나의 timestamp 모두를 수정해주어야 했고, 수정을 마친 데이터는 map 함수를 이용한 배열 렌더링을 위해서 하나의 배열에 집어넣어야 했다.

따라서 일단 forEach 함수를 이용하여 모든 데이터를 순회, 문서 하나를 꺼내와서 빈 객체에 집어넣고 문제가 되는 timestamp 부분을 수정한 다음에 빈 배열에 push하는 작업을 반복하여 문제를 해결할 수 있었다.



5. 결론.

나의 경우에는 내가 다루는 데이터가 그렇게 복잡하지도 않았고 (나에게는 복잡했지만 실무자 수준에서는..) 리덕스를 사용하면서 리덕스 개발자 도구를 적용했었기에 데이터의 흐름을 얼추 파악할 수 있었다. 이것이 아니었다면 에러를 해결할 수 없었을 것이며 시간을 들여 구현했던 프로젝트 전체를 폐기해야하는 심각한 상황에 처했을 수도 있었다.

다루는 데이터가 많아질 수록, 데이터의 흐름이 복잡해질 수록 이들을 관리하는 구조는 효율적이고 직관적이어야 한다. 또한 다루는 데이터의 형식이 어느 시점에서 어떻게 변화하는지를 정확하게 파악할 줄 알거나, 유사시 파악이 가능한 시스템을 갖추어야 한다.

profile
프론트엔드 개발자를 준비하고 있습니다.

0개의 댓글