1. 불변함수
상태 업데이트를 지속적으로 하는 React 에서 불변성을 유지하는 것이 굉장히 중요하다.
리액트에서 개발의 편의성을 위해 컴포넌트 단위로 쪼개서 함수를 만든다. 큰 도화지에 스티커를 붙이듯 부모에 해당하는 함수의 return값에 컴포넌트들을 넣어 화면을 구현한다. 대체로 부모가 변경될 때 자식은 그에 의존적이기 때문에 부모에 입력된 컴포넌트들을 변경하는 연산을 수행할 경우가 생긴다.
그런데 만약 부모에 의해서 컴포넌트의 원본이 마구잡이로 바뀌게 된다면 프로그램의 안정성이 떨어지게 될 것이다. 리액트 프로그램의 안정성을 위해 불변성이 필요하다. 불변성을 유지함으로써 연산을 최적화시킬 수 있다!
불변성의 정의는 상태를 변경하지 않는 것을 뜻하며 컴퓨터 공학적으로 풀어 말한다면 메모리 영역의 값을 변경할 수 없는 것을 말한다. 불변함수를 간단히 말하자면 깊은 복사를 해주는 함수를 말한다.
불변성을 지켜줌으로써 외부의 값을 함부로 변경할 수 없게 하여 예상치 못한 오류를 방지하고 프로그래밍의 구조를 단순하게 유지할 수 있다.
자바스크립트는 함수의 파라미터의 인자가 원시 데이터 타입과 객체일 때 동작방법이 달라진다.
원시 데이터 타입의 경우 변수에 데이터가 새로운 주소에 할당되기 때문에 불변성이 유지된다
반면, 객체 타입일 때는 데이터의 값이 heap에 저장되며 변수에 heap 메모리의 주소값이 할당되기 때문에 얕은 복사가 되어 변수는 객체의 주소를 가리키게 된다. 변할 수 있는 값이 되는 것이다.
결국 의도적으로 불변성을 지켜주어야하는데 이것이 불변함수의 역할이다. 불변함수는 새로운 주소 값을 가진 객체를 생성하여 상태를 업데이트 해준다. spread operator, map, filter, slice, reduce 메소드들을 활용할 수 있다.
2. 배열의 깊은 복사(deep copy)
- Spread 연산 - 복사하기
'...' 을 통해 배열의 요소를 흩뿌려 새로운 주소에 배열을 할당할 수 있다.
const a = [1, 2, 3];
const b = [...a];
const c = [0, ...a, 4];
b.push(4);
console.log(`a의 값: ${a}`);
console.log(`b의 값: ${b}`);
console.log(`c의 값: ${c}`);
a의 값: 1,2,3
b의 값: 1,2,3,4
c의 값: 0,1,2,3,4
위와 같이 b에 push연산을 해도 깊은 복사이기 때문에 a의 배열값이 변하지 않는 것을 확인할 수 있다.
- concat() 연산 - 추가하기
배열 객체의 메소드인 concat()을 사용하면 깊은 복사를 할 수 있다. 값을 추가할 때 사용한다.
const a = [1, 2, 3];
const b = a.concat(4)
console.log(`a의 값: ${a}`);
console.log(`b의 값: ${b}`);
a의 값: 1,2,3
b의 값: 1,2,3,4
- cf) 만약 push() 메소드를 사용한다면?
const a = [1, 2, 3];
const b = a.push(4)
console.log(`a의 값: ${a}`);
console.log(`b의 값: ${b}`);
a의 값: 1,2,3,4
b의 값: 4
- filter() 연산 - 걸러내기
특정 값을 삭제하여 깊은 복사를 할 때 사용한다.
const a = [1, 2, 3];
// 익명 함수 사용
// filter는 bool을 return 받는다. true에 해당하는 값만 저장함
const b = a.filter((n)=> {return n != 1});
console.log(`a의 값: ${a}`);
console.log(`b의 값: ${b}`);
a의 값: 1,2,3
b의 값: 2, 3
- slice() 연산 - 잘라내기
slice(strat, end)는 start 인덱스부터 (end-1) 인덱스까지 잘라내 깊은 복사를 해준다.
...(spread)를 잘 이용하면 2차원 배열이 되는 것을 막을 수 있다.
const a = [1, 2, 3];
const b = a.slice(0, 2);
const c = [a.slice(0, 1), 5, a.slice(2, 3)]
const d = [...a.slice(0, 1), 5, ...a.slice(2, 3)]
console.log(`a의 값: ${a}`);
console.log(`b의 값: ${b}`);
console.log(`c의 값: ${c}`);
console.log(b);
console.log(c);
console.log(d);
a의 값: 1,2,3
b의 값: 1,2
c의 값: 1,5,3
[1, 2]
[Array(1), 5, Array(1)] => [[1], 5, [3]]을 의미
[1, 5, 3]
- map() 연산 - 반복하기
map의 기능은 forEach와 비슷하지만 리턴이 있다는 점에서 다르다.
forEach() 반복문은 리턴이 없기 때문에 익명 함수 안에 push를 넣어줘야 한다..
const a = [1, 2, 3];
// const b = [];
// a.forEach((n)=>{ b.push(n) });
const b = a.map((n)=>n);
b.push(5);
console.log(`a의 값: ${a}`);
console.log(`b의 값: ${b}`);
a의 값: 1,2,3
b의 값: 1,2,3,5
3. 객체의 수정
- 객체의 수정
const data = { name: "Lim", phone: "9999" }
const a = {id:1, name:"Hong", phone:"1111", age:17, gender:"male"}
// const b = {...a, name:"Lim", phone:"9999"}
const b = { ...a6, ...data}
const c = {...data, ...a6}
console.log(b)
console.log(c)
{id: 1, name: 'Lim', phone: '9999', age: 17, gender: 'male'}
{name: 'Hong', phone: '1111', id: 1, age: 17, gender: 'male'}
- 객체 배열의 수정
아래는 원본 데이터를 바꿔버리기 때문에 불변성이 유지되지 않는다는 문제가 있다.
const users = [
{ id: 1, name: "Park", phone: "2222" },
{ id: 2, name: "Lee", phone: "3333" },
{ id: 3, name: "Bang", phone: "4444" },
];
const updateUserDto = {
id:2, name:"Hong"
}
users[1].name = updateUserDto.name
console.log(users)
map을 이용해 새로운 객체를 생성하고 삼항연산자와 Spread를 이용해 수정해주자.
아래의 결과를 확인해보면 새로운 객체인 newUsers만 수정된 것을 확인할 수 있다.
const users = [
{ id: 1, name: "Park", phone: "2222" },
{ id: 2, name: "Lee", phone: "3333" },
{ id: 3, name: "Bang", phone: "4444" },
];
const updateUserDto = {
id:2, name:"Hong"
}
const newUsers = users
.map(user => user.id === updateUserDto.id ? {...user, ...updateUserDto}: user);
console.log(users)
console.log(newUsers)
[{id: 1, name: 'Park', phone: '2222'}, {id: 2, name: 'Lee', phone: '3333'}, {id: 3, name: 'Bang', phone: '4444'}]
[{id: 1, name: 'Park', phone: '2222'}, {id: 2, name: 'Hong', phone: '3333'}, {id: 3, name: 'Bang', phone: '4444'}]