React에서 state 배열 관리 및 반복문에서의 state
React 프로젝트를 진행하면서 state를 배열로 관리해야 하는 경우와 반복문에서 state 주의사항 정리.
프로젝트를 진행하면서 state에 배열을 담는 경우나 배열 구조의 state를 제어해 삭제 또는 추가해야 하는 경우가 있었다.
처음 리액트를 배울때나 프로젝트 초반에는 axios로 응답받은 데이터를 state에 set 해주는 케이스만 있었기 때문에 배열 구조의 state 관리에 크게 문제도 없었고 고민도 없었다.
하지만 이미지 파일 관리에 대해 구현하면서부터 문제가 생기기 시작했다.
state는 readOnly이므로 state 자체를 수정할 수 없다.
그럼 setState를 통해 수정된 값을 다시 넣어줘야 하는데 어떻게 처리할 것인가에 대한 문제였다.
그리고 이 문제를 어떻게 할까 고민하면서 추가적으로 문제가 생긴게 배열에 어떻게 담고 어떻게 삭제하지? 라는 문제였다.
그래서 이 문제에 대해 가장 근본적인 부분부터 정리하고자 한다.
Javascript에서의 배열
배열 생성이나 제어에 대해서는 사실 React에서의 배열이라고 하기 보다는 ES6 문법이다.
ES6 문법에 대해 계속해서 공부하고 있긴 하지만 이런 활용에 대해서는 무지했다.
이번에 알게된 것에 대해 코드를 먼저 정리한다.
//빈 배열 선언
const arr = [];
//전개 연산자
const arr = [...value];
//배열의 끝에 추가
arr.push(value);
//배열의 앞에 추가
arr.unshift(value);
//배열의 2번 인덱스 위치에 추가
arr.splice(2, 0, value);
//배열의 끝에 위치한 요소 제거
arr.pop();
//배열의 끝에 위치한 요소를 제거하고 변수에 받음
const popValue = arr.pop();
//배열의 첫번째 요소 제거
arr.shift();
//배열의 첫번째 요소를 제거하고 변수에 받음
const shiftValue = arr.shift();
//배열의 2번 인덱스부터 1개의 요소를 제거
arr.splice(2, 1);
//배열의 2번 인덱스부터 1개의 요소를 제거하고 값을 변수에 받음
const removeValue = arr.splice(2, 1);
//delete를 통한 삭제
delete arr[1];
//함수를 사용하지 않는 방법
//배열의 끝에 요소를 추가
arr[arr.length] = value;
//배열의 크기를 줄임(가장 끝 요소가 삭제)
arr.length = arr.length - 1;
//요소 추가
//길이 3의 arr에 5 인덱스 value를 넣도록 한다면
//4번 인덱스의 값은 undefined로 생성
arr[5] = value;
가장 먼저 전개연산자.
전개 연산자는 배열 또는 객체를 넘기는 용도로 사용한다.
앞에 ...이 붙게 되고 새로운 배열에 [...values] 이렇게 초기화를 하는 경우 values라는 배열 또는 객체에 담겨있던 데이터가 새로운 배열에 들어가게 된다.
자바스크립트 배열에 대해 알아보면서 편하다고 생각했던 점이 크기를 지정하지 않아도 된다는 점이었다.
물론 잘못사용하면 독이 될만한 문제이긴 하지만 그래도 편한건 편하다..
자바스크립트 배열에서는 배열의 끝이나 가장 앞에 요소를 추가할 수 있다.
.push(value)를 통해 가장 끝에 요소를 추가할 수 있고, .unshift(value)를 통해 가장 앞에 요소를 추가할 수 있다.
이걸 알게 되면서 느낀건데 덱(deque)의 개념이지 않나? 라는 생각이 들었다.
삭제의 경우 pop(), shift()로 가장 끝, 가장 앞 요소를 제거 및 추출할 수 있다.
splice 함수는 요소를 추가할수도, 삭제할수도 있다.
요소의 추가는 splice(index, 0, [요소1, 요소2]) 이렇게 사용하고,
요소의 삭제는 splice(index, 개수) 이렇게 사용한다.
추가의 경우 index의 위치에 요소를 추가하게 된다.
요소는 꼭 배열이 아니어도 되고 단일 객체여도 상관없다.
삭제 splice는 index 위치에서 개수만큼 요소를 제거한다.
삭제 처리를 하는 splice는 제거한 요소를 반환받을 수 있다.
삭제 처리는 delete를 통해서도 처리할 수 있는데 delete를 통한 처리는 해당 위치 값만 삭제된다.
즉, arr = [1, 2, 3, 4, 5] 가 있다고 하고 splice(2, 1)을 처리하면 arr = [1, 2, 4, 5] 가 되지만
delete arr[2]를 하게 되면 arr = [1, 2, undefined, 4, 5]가 된다.
그래서 잘 구분해서 사용해야 할 것 같다.
함수를 사용하지 않고 추가 삭제도 가능한데 index 부분에 length를 넣어주면 가장 끝에 요소가 들어가게 된다.
그리고 배열의 length를 length - 1 로 수정하게 되면 크기가 1 줄어들면서 가장 끝 요소가 삭제된다.
마지막으로 정말 특이했던 것 중 하나.
arr = [1, 2, 3]이라고 하고 arr[5] = 6 이렇게 처리하게 되면
arr = [1, 2, 3, undefined, undefined, 6] 이렇게 처리가 된다.
입력한 그 위치에 요소를 넣어주긴 하지만 기존 배열에서 끝 Index의 위치와 차이가 발생한다면 undefined를 넣고 해당 인덱스에 요소를 넣어준다.
React 에서 state 배열 관리
useState는 배열 객체를 받고 관리할 수 있다.
보통 axios 요청 응답을 state에 담아 map으로 풀어 사용했다.
근데 이미지 파일 관리를 해야 되기 시작하면서 배열을 제어할 수 있어야 했다.
사용자가 처음 이미지 파일을 선택했을 때는 state도 비어있고 배열화 해서 넣어주면 간단하겠지만 이후 사용자가 그 중 일부를 삭제하거나 추가하는 경우 배열을 제어할 수 있어야 한다고 생각했다.
state는 readOnly로 동작하기 때문에 수정이 불가능할 것이라고 생각했고 setState를 통해 새로운 배열을 넣어줘야 겠다고 생각했기 때문이다.
state을 배열 구조로 계속해서 추가와 삭제를 할 수 있도록 관리하기 위해서는 전개 연산자를 사용하면 된다.
이벤트가 발생했을 때 전개 연산자를 통해 배열구조의 state를 새로 생성되는 배열에 담아주고 그 배열내에서 추가 또는 삭제 처리가 이루어진 뒤 setState를 처리하면 된다.
예시코드
//files는 배열구조 state
//[ {fileNo: 1, file: file1}, {fileNo: 2, file: file2}, ... ]
const [files, setFiles] = useState([]);
//사용자의 이미지 파일 선택 이벤트 핸들링
const handleInputOnChange = (e) => {
//fileArray 배열을 생성하면서
//전개 연산자를 통해 초기값으로 files를 담도록 처리
const fileArray = [...files];
//선택된 파일 리스트
const fileList = e.target.files;
//배열에 파일 추가 처리
for(let i = 0; i < fileList.length; i++) {
//push로 파일 배열에 요소 추가
fileArray.push({
fileNo: ++previewNo,
file: fileList[i],
});
}
//setState로 배열을 state에 저장
setFiles(fileArray);
}
//사용자의 선택 이미지 삭제 이벤트 핸들링
const handleDeleteOnClick = (e) => {
//삭제할 파일 fileNo
const deleteNo = Number(e.target.getAttribute('value');
//fileArray 배열을 생성하면서
//전개 연산자를 통해 초기값으로 files를 담는다.
const fileArray = [...files];
//삭제할 요소를 찾는다.
//find 함수를 사용해 fileNo가 deleteNo와 일치하는 요소를 찾아 변수에 담아준다.
const delObject = fileArray.find(function (item) {
return item.fileNo === deleteNo;
});
//find 함수를 통해 찾은 객체가 몇번 인덱스에 위치하는지 찾는다.
const delIndex = fileArray.indexOf(delObject);
//splice 함수를 통해 삭제하고자 하는 인덱스 위치의 요소를 삭제한다.
fileArray.splice(delIndex, 1);
//삭제가 완료된 배열을 state에 저장
setFiles(fileArray);
}
배열구조의 state 제어는 이런방법으로 할 수 있었다.
state가 readOnly라 직접 제어할 수 없으니 배열로 꺼낸 뒤에 제어하고 다시 setState로 넣어주는 방법으로 처리할 수 있다.
반복문에서의 state
배열 구조의 state를 제어하면서 가장 먼저 시도해봤던 방법은 반복문을 통한 state 관리였다.
가장 먼저 사용자가 선택한 파일을 state에 담아줄 때 반복문을 통해서 처리하면 되겠다는 생각을 했었다.
const [files, setFiles] = useState([]);
const handleInputOnChange = (e) => {
//[ {file1}, {file2}, {file3}]
const fileList = e.target.files;
for(let i = 0; i < fileList.length; i++){
setFiles([...files, fileList[i]]);
}
}
이 처리가 완료된 후 files를 콘솔에 찍어보면 가장 마지막 파일인 3번 파일만 들어가있는 것을 확인할 수 있다.
이유는 setState는 호출되는 즉시 상태를 업데이트 하지 않고 Promise를 통해 비동기적으로 상태를 변경시키기 때문이다.
즉, file1, file2, file3을 set 했다고 하더라도 반복문이 끝나기 전에 state가 변경되지 않았기 때문에 전개 연산자로 files를 불러와도 비어있는 또는 원래 files데이터만 들어오기 때문에 file1, file2에 대한 set은 수행되지 않게 되어 마지막 호출인 file3만 들어가게 된다.
좀 더 세부적으로 보자면 files state가 빈 배열 상태에서 수행한다고 했을 때
//[ {file1}, {file2}, {file3} ]
const fileList = e.target.files;
// i = 0
// ...files = []
// fileList[0] = file1
// setFiles([], file1) -> 변경되지 않는다.
// i = 1
// ...files = []
// fileList[1] = file2
// setFiles([], file2) -> 변경되지 않는다.
// i = 2
// ...files = []
// fileList[2] = file3
// setFiles([], file3) -> 변경되지 않는다.
// 반복문 종료.
// 마지막 요청이었던 setFiles([], file3) 으로 state 변경.
// 결과 -> files = [ { file3 } ]
for(let i = 0; i < fileList.length; i++){
setFiles([...files, fileList[i]]);
}
이런 과정을 거치게 된다.
원래 생각대로라면 i = 1일때 ...files를 통해 file1이 들어와야 한다고 생각했지만 set이 수행되지 않았기 때문에 계속 시작값인 빈 배열이 들어와 데이터가 쌓이지 않는 것.
그래서 반복문을 통해 setState를 사용하는 것이 아닌 반복문을 통해 state에 담아줄 객체를 만든 뒤 반복문이 모두 종료된 이후 결과물을 state에 넣어주도록 해야 한다.
공식 문서에서도 반복문, 조건문, 중첩된 함수내에서는 hook을 호출하지 말라고 한다.