관리 메뉴

bright jazz music

체크박스 로직(발송예약/수정: 체크 상태가 유지돼야 하는 경우) 본문

Framework/ReactJs

체크박스 로직(발송예약/수정: 체크 상태가 유지돼야 하는 경우)

bright jazz music 2024. 4. 15. 16:40

AlarmSendPage:

알람 발송을 예약하는 페이지. 여러 개의 셀렉트 박스와 '수신자'라는 textarea 있다.

AlarmAndSmsModal:

'개인회원' 또는 '법인회원'을 검색하기 위한 모달창. 알람발송과 문자발송 페이지에서 공통으로 사용한다.
기본적으로 이 모달은 검색창, 검색버튼, 테이블 헤더로 이루어져 있다. 검색어를 넣고 검색하면 결과가 테이블 헤더 밑에 <td> 행으로 보여진다.
이 행들은 기본적으로 가장 좌측에 체크박스를 가지고 있다. 

   <input type="checkbox" checked={checkedItems[el.user_id]} onChange={() => handleCheckboxChange(el.user_id)} />

만약 사용자가 특정 행의 체크박스를 체크했을 경우, 사용자 아이디가 이 모달의 부모 컴포넌트인 AlarmSendPage의 '수신자' textarea에 보여져야 한다.
이 때 보여지는 값은 'userid01,userid02'의 스트링 값이며, 사용자가 타이핑을 통해 입력할 수 있거나 지울 수 있어야 한다.


---------

- 부모 컴포넌트인 AlarmSendPage와 자식 컴포넌트인 AlarmAndSmsModal 모달이 값을 공유해야 하므로 AlarmSendPage에서 상태를 선언해준다.

  
  const [checkedItems, setCheckedItems] = useState({});
  const [receiverList, setReceiverList] = useState([]);




  checkedItems는 모달의 체크박스를 관리하기 위한 객체 상태이다.
  receiverList는 모달에서 체크된 값들의 데이터(여기서는 user_id)를 저장하기 위해 선언한 배열 상태이다.
참고로 '수신자' textarea에 보여지는 'userid01,userid02' 값은 recevierList.join(',') 값이다. 

즉 ['userid01', 'userid02'] ==> 'userid01,userid02' (서버에서 이렇게 받기를 원했다.)

  이를 모달에서 사용할 수 있도록 넘겨준다. 상태와 setState함수를 함께 파라미터로 넘겨주었다.

  const [modal, setModal] = useState(false);
  
  ...


        {modal && (
        <AlarmAndSmsModal
          checkedItems={checkedItems}
          setCheckedItems={setCheckedItems}

          receiverList={receiverList}
          setReceiverList={setReceiverList}
        
          modalId={modalId}
          modalOnOff={handleModalOnOff}
        />
      )}


모달은 이 값들을 다음과 같이 사용해야 한다. 우선 모달이 처음 켜졌을 때는 아무 일도 일어나지 않는다. 아직 검색하지 않았기 때문에 서버에서 값을 받아오지 않았기 때문이다.
이 값들은 사용자가 검색어를 입력하고 검색버튼을 누른 뒤, 서버에서 response가 도착하면 그 때부터 사용된다.


서버에서는 반환값이 성공적으로 도착하면 const response변수에 할당해준다. response 변수는 JSON이 담겨있다. response.data.resultList은 자바스크립트 객체를 원소로하는 배열이다.
이 배열을 담을 상태로는 AlarmAndSmsModal에서 선언한 tableData를 사용한다. 

const [tableData, setTableData] = useState([]);

...

setTableData(response.data.resultList);


그러면 그러면 tableData의 상태값이 바뀌면서 아래의 useEffect가 발동된다. 
먼저 receiverList에 담겨있는 user_id가 있는지 확인한다.
이유는 예약 발송을 수정할 때, 서버에서 반환한 값에는 수신자 리스트가 존재할 것이고 
그 값들은 AlarmAndSmsModal 모달에서 검색했을 때 그 이미 체크돼 있어야 하기 때문이다.

예) 서버가 receiverList의 값으로 'userid01,userid02'을 반환함. userid01로 검새창에서 검색했을 때는 이미 체크 된 상태여야 함.
만약 해당 로직이 없다면 검색했을 때 해당 아이디의 체크박스가 체크되어 있지 않을 것이다. 뒤에 얘기하겠지만 이는 큰 문제를 야기한다.



useEffect(() => {

    setCheckedItems((prevItems) => {
      const newItems = { ...prevItems };
      
      // tableData의 각 아이템에 대해 user_id가 receiverList에 포함되어 있는지 확인하고, 포함된 사용자는 checkedItems 객체에 true값으로 추가한다.
      
      tableData.forEach((item) => {
        if (receiverList.includes(item.user_id)) {
          
          newItems[item.user_id] = true;
        }


        // else if (!Object.prototype.hasOwnProperty.call(newItems, item.user_id)) {
        //   // 새로운 사용자는 false로 초기화
        //   newItems[item.user_id] = false;
        // }


      });

      // 만약 receiverList에 존재하지 않은 값들은 checkedItems에 추가하지 않는다.

      return newItems;
    });
}, [tableData, receiverList, setCheckedItems]);



만약 receiverList에 원소(사용자 아이디)가 존재한다면, newItems 객체에 "사용자아이디":true 형태로 속성을 추가한다. 

예시) {"userid01": true, "userid02": true}

이 값이 반환되고 setCheckedItems(newItems)처리되면서 최종적으로 checkedItems 객체의 상태가 변경되는 것이다.



--

이 로직이 필요한 이유를 구체적으로 생각해 보자. 

체크박스의 경우 일반적으로는 아래의 로직을 따른다.

- 검색어 검색(요청)
- 서버가 응답 반환(일반적으로는 배열)
- 반환된 배열의 원소를 확인하여, 고유한 값(seq, id 등)을 기준으로 객체를 구성(일반적으로 reduce 함수 사용)
  {"고유값": false} 이런 식으로 구성한다. 

  setCheckedItems(tableData.reduce((acc, item) => ({ ...acc, [item.user_id]: false }), {}));

  따라서 checkedItems는 아래와 같은 형태를 띄게 된다.
  
  {
    "userid01": false,
    "userid02": false,
      
      ...

    "userid09": false,
    "userid10": false,
  }

- 만약 사용자가 특정 데이터를 체크한 경우 아래와 같이 checkedItems 객체의 속성을 반전시킨다.
  
  
  const handleCheckboxChange = (dataValue) => {
  
    // 기존의 체크박스 상태를 유지하면서 특정 dataValue에 대한 상태만 업데이트하기 위해 ...currentItems 사용
    setCheckedItems((currentItems) => ({
      ...currentItems,
      [dataValue]: !currentItems[dataValue],
    }));

  
    // 체크된 값을 배열로 만들어 사용하기 위해 사용하는 함수:
    receiverListArrayChange(dataValue);
  };

  ...

  반복문===>
  <input type="checkbox" checked={checkedItems[el.user_id]} onChange={() => handleCheckboxChange(el.user_id)} />

  ...


  만약 userid01 행의 체크박스를 체크한다면 위 로직을 거쳐 checkedItems 객체는 아래와 같이 변경된다.

  {
    "userid01": true,
    "userid02": false,
      
      ...

    "userid09": false,
    "userid10": false,
  }


---------------------

서버에 receiverList를 보내는 로직은 여기에 적지 않았다. 만약 이것이 서버에 보내고 종료되는 로직이라면 문제가 없다.
그런데 만약 서버에 이미 전송한 데이터를 수정하는 경우 문제가 발생한다. 아래 절차를 거치지 않기 때문이다.

- 모달창 검색 -> 서버에서 배열 수신 -> 배열을 기준으로 체크리스트 객체 구성

서버에서는 receiverList 배열만 반환하기 때문에 검색모달을 열어 검색할 필요가 없다.
따라서 서버에서 배열 응답을 주지 않는다. 따라서 checkedItems도 빈 객체인 채로 남아있다.

이러한 이유로 checkedItmes와 receiverList의 구성이 달라지는 것이다.

만약 "userid01"이라는 사용자에게 보낸 알람 예약을 수정한다고 가정해보자.

receiverList에는 "userid01"이 들어있다. checkedItems는 빈 객체이다. 이 상태에서 검색 모달을 열어 "userid01"를 검색한다고 가정하자.

- 서버에서 "userid01" 키워드가 포함 배열을 응답으로 반환한다.
- checkedItems 객체가 다음과 같이 구성된다. 

  {"userid01": false}


문제는 receiverList 배열에는 이미 "userid01"이 들어가 있다는 것이다. 이 상태에서 해당 행의 체크박슬르 체크한다고 하면 아래와 같이 된다.

  - checkItems가 변경된다. ==> {"userid01": true}
  - receiverList가 변경된다. ==> []

"userid01" 행이 체크되었는데 실제 "수신자" 리스트에서는 "userid01"이 제거되는 것이다. 사용자의 의도와 정확히 반대되는 결과가 발생하는 것이다.


---------------------







이러한 문제를 방지하기 위해서, 서버 응답 배열을 기준으로 checkedItems 객체를 구성할 때, receiverList 배열을 확인해서 
그 원소들을 checkedItems의 프로퍼티로 추가한다.

("발송예약"과 "수정"에 대한 로직을 따로 구현해도 되지만, 여기서는 "발송예약"과 "수정" 메뉴에 동일한 컴포넌트를 사용하고 있기 때문에 그렇다.)








---------------------

서버에서는 ","으로 연결된 스트링 값을 요구하므로, receiverList를 보낼 때는 receiverList.join(',') 값을 보낸다. "userid01,userid02" 와 같은 형태가 된다.



---------------------






부가설명1 :

const handleCheckboxChange = (dataValue) => {
  
    // 기존의 체크박스 상태를 유지하면서 특정 dataValue에 대한 상태만 업데이트하기 위해 ...currentItems 사용
    setCheckedItems((currentItems) => ({
      ...currentItems,
      [dataValue]: !currentItems[dataValue],
    }));

  
    // 체크된 값을 배열로 만들어 사용하기 위해 사용하는 함수:
    receiverListArrayChange(dataValue);
  };


: 검색 모달창에서 A라고 검색한 뒤에 몇 개를 체크한다. B를 검색한다. 다시 A를 검색한다.
이 때 A 검색어에서 체크된 값들이 체크돼 있으려면 기존의 checkedItmes 객체에 체크된 값들이 계속 저장돼 있어야 한다.
만약 

  setCheckedItems((currentItems) => ({ ...currentItems, [dataValue]: !currentItems[dataValue]}));

대신 아래의 로직을 사용한다면

  setCheckedItems((currentItems) => ({ [dataValue]: !currentItems[dataValue] }));

서버에서 값을 받아올 때마다 checkedItmes가 초기화 될 것이다.

---------------------

부가설명2 :

const handleReceiverList = (event) => {
    const input = event.target.value;

    if (input.trim() === '') {
      setReceiverList([]);
    } else {
      const newList = input.split(',').map((item) => item.trim());
      setReceiverList(newList);
    }
};


...


<TextareaCustomed
  $fontSize="0.8rem"
  $borderRadius="4px"
  $width="100%"
  $height="5rem"
  $resize="vertical"
  value={receiverList || ''}
  onChange={(event) => handleReceiverList(event)}
  disabled={isDisabled}
/>


"수신자" textarea에서 사용자가 키보드 입력을 통해 수신자를 추가하거나 제거하기 위해 사용하는 로직이다.

---------------------
  
  


Comments