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에서 사용자가 키보드 입력을 통해 수신자를 추가하거나 제거하기 위해 사용하는 로직이다.
---------------------