본문 바로가기
FrontEnd/React

React 애플리케이션에서 모달(Dialog)이 매핑된 배열의 마지막 요소만 표시되는 문제

by 감중에홍시 2023. 7. 24.

문제인식 & 원인 분석

reportList 배열의 길이에 따라 Card 컴포넌트를 만든 후, 각 컴포넌트에 csv 파일을 다운로드 하는 기능(버튼)(redux, dispatch를 이용하여 요청) + 다운로드를 하겠냐고 물어보는 모달창을 개발하고 있었다.

다운로드 요청을 하기 위해서는 아래 코드에서처럼 다운로드 하려는 컴포넌트의 id 값을 이용하여 dispatch 요청을 해야한다. 따라서 동적으로 생성된 배열의 각 요소에 모달이 잘 매핑되어야 한다.

다운로드 버튼을 누를 시 띄워지는 모달창

 dispatch({
  type: LOAD_FILE_DATA_REQUEST,
  data: {
    id: report.id - 1, // 다운 받으려 하는 report의 id 값을 담아 보낸다.
    fileList: [lastFile.fileName], // 마지막 파일을 다운로드 할 수 있게 파일명을 배열에 담아 보낸다.
  },

그런데 3개의 컴포넌트 중 마지막 컴포넌트에서만 다운로드 기능이 정상 동작 하는 것이다. 첫 번째와 두 번째의 컴포넌트에서는 다운로드 기능이 정상 작동되지 않았다.

xhr 요청을 확인해 보니, DB에 해당되는 파일이 없다는 요청이 날아왔고, 나는 이내 api 요청 자체를 잘못 보내고 있음을 확인할 수 있었다.

컴포넌트 1에 대해서 api 요청을 보낼 때에는 reportId가 1, 컴포넌트 2에 대해서는 reportId가 2, 3에 대해서는 3을 파라미터로 넣어서 보내야 하는데, 보내는 요청을 console로 찍어보니 아래와 같이 마지막 번호 3에 대해서만 계속 요청을 보내는 것을 확인할 수 있었다.

reportList의 길이에 따라 컴포넌트를 만든 후, 컴포넌트의 report.id를 Confirm 컴포넌트(모달창)를 통해서 요청을 보내면, 해당 컴포넌트에 속하는 파일을 다운로드 하게 해주는 코드 구조(수정 전 코드)

 

원래는 1, 2, 3을 보내야 하는데 계속 reportId 3만 파라미터로 전달되는 모습

 

 

문재 해결

영어로 검색하였더니 역시 스택오버플로우에 나와 비슷한 문제를 겪는 사례를 확인할 수 있었다.

https://stackoverflow.com/questions/66532393/modal-only-shows-last-element-of-mapped-array-in-react

이 분도 역시 나처럼 배열 길이만큼 모달창을 매핑하여 띄워주는 기능을 개발하고 있었는데 마찬가지로 배열의 마지막 정보에 대한 모달창만 띄워지는 현상이 있었다.

이러한 문제가 발생하는 원인은 클로저(Closure)와 관련이 있다. React에서 배열을 매핑하여 컴포넌트를 생성할 때, 해당 컴포넌트 내부에 이벤트 핸들러 등의 함수를 선언하면 클로저가 형성된다. 클로저는 함수와 그 함수가 선언된 환경(Lexical environment)의 조합으로, 함수가 자신이 선언된 위치에서 주변 환경의 변수와 함수들을 기억하고 접근할 수 있게 한다.

매핑된 배열의 각 요소 컴포넌트가 모달을 열 때, 이벤트 핸들러에서 사용하는 변수들은 클로저에 의해 저장되고 나중에 호출될 때 해당 변수들을 참조한다. 그러나 이벤트 핸들러의 클로저는 반복문에서 마지막 요소만을 기억하게 되므로, 매핑된 배열의 모든 요소가 아닌 마지막 요소에 대한 정보만 사용된다. 결과적으로, 모든 모달이 같은 정보를 표시하게 되는 문제가 발생한다.

개선방안

(현) 여러 모달창이 생성되는데 마지막 정보만 저장됨  ----> (수정방향) 모달창을 하나만 생성하고, 컴포넌트의 id값을 state에 저장한 후, id 값을 모달창에 전달

해당 문제를 해결하기 위해서는 클로저가 형성되는 상황을 제대로 처리해야 한다. 각 요소에 대한 모달이 개별적으로 동작하도록 클로저를 정상적으로 설정해야 한다. 해결 방법은 다음과 같다.

모달을 열 때, 필요한 정보를 클로저에 저장하는 대신, 컴포넌트의 id를 state에 저장한다.state를 사용하면 각 요소 컴포넌트가 개별적인 상태를 유지하며 동작하므로 문제를 해결할 수 있다.

따라서 나는 모달창이 열릴 때, 해당 컴포넌트의 id를 저장하는 state를 만든 후, 저장하는 방식을 사용하였다.

수정 후 코드

const [selectedDialogId, setSelectedDialogId] = useState<number>(null); // reportId 값을 저장하기 위한 state 추가
...
// 파일 다운로드 버튼. 이 버튼을 누르면 해당 컴포넌트에 해당하는 reportId 값을 state에 저장하고
// 다운로드를 하겠냐는 모달창을 띄워준다.
const FileDownloadButton = ({ reportId }) => {
    return (
      <Tooltip title={translate('log-page.icon.downBtn')} arrow>
        <IconButton
          color="inherit"
          onClick={(e) => {
            if (IsDataExport()) {
              e.preventDefault();
              setOpenDialog(true);
              setSelectedDialogId(reportId); // 이 곳에 reportId에 대한 값 저장하여 보관
              dispatch({
                type: LOAD_REPORT_FILE_LIST_REQUEST,
                data: { reportId },
              });
            } else {
              dispatch({
                type: COM_SNACKBAR,
                open: true,
                message: translate('WARN.WWE00007'),
                variant: 'error',
              });
            }
          }}
        >
          <GetApp style={{ fontSize: 40 }} />
        </IconButton>
      </Tooltip>
    );
  };

그리고 reportId를 전달 받을 수 있는 모달창이 하나만 존재해야 함으로 Confirm 컴포넌트를 map 함수 밖으로 빼주었다.

(수정 후)하나의 모달창을 만들기 위해 map 함수 밖으로 Confirm 컴포넌트를 뺀 코드

이렇게 코드를 수정한 후, 테스트를 해보니 해당 컴포넌트에 맞게 reportId 값이 파라미터로 잘 전달되고, csv 파일이 잘 다운로드 되는 것을 확인할 수 있었다.

reportId 값이 컴포넌트의 순서에 맞게 잘 전달되는 모습
각 컴포넌트에 해당되는 csv 파일들이 정상적으로 다운로드 되는 모습

 

결론

React 애플리케이션에서 배열의 각 요소에 모달을 매핑하는 상황에서 클로저로 인해 발생하는 문제를 해결하기 위해서는 state(useState)를 활용하거나 각 요소에 대한 고유한 식별자를 생성하여 클로저가 정상적으로 동작하도록 조치해야 한다.

이를 통해 각 요소에 대한 모달이 개별적으로 동작하게 구현할 수 있다.

React 애플리케이션에서 발생하는 문제를 클로저와 상태 관리를 이용하여 해결하는 방법을 익히면, 코드의 유지보수성과 효율성을 높일 수 있다.