하지만 얼마 후 이상한 문제가 보이기 시작합니다.
변경하지 않았을 때 값이 변경되는 변수.
"나는 그것을 복사했다고 생각했습니다! 왜 값이 변했어요?” <- 참조 버그가 있습니다!
이 게시물이 끝나면 왜 그런 일이 발생하고 해결 방법을 이해하게 될 것입니다.
변수는 상자가 아닙니다. 변수는 상자를 가리키는 이름표 입니다.
참조(Reference)가 뭐죠?
let word = "hello"
단어가 "hello"가 있는 상자를 어떻게 가리키는지 주목하세요.
여기에 간접 계층이 있습니다.
변수는 상자가 아닙니다. 변수는 상자를 가리키는 이름표 입니다.
이제 할당 연산자 =를 사용하여 이 변수에 새 값을 지정해 보겠습니다.
word = "world"
함수 매개변수에 값을 할당하려고 시도한 적이 있다면 이것이 함수 외부의 어떤것도 변경하지 않는다는 것을 깨달았을 것입니다.
이런 일이 발생하는 이유는 함수 매개변수를 재할당하면 전달된 원래 변수가 아닌 지역 변수에만 영향을 미치기 때문입니다.
다음은 예입니다.
function reassignFail(word) {
// this assignment does not leak out
word = "world"
}
let test = "hello"
reassignFail(test)
console.log(test) // prints "hello"
할당(word = "world") 후에 단어 변수는 새 값 "world"를 가리킵니다. 그러나 우리는 test를 변경하지 않았습니다.
test 변수는 여전히 이전 값을 가리킵니다.
타입의 두 카테고리
JavaScript에는 두 가지 타입의 광범위한 범주가 있으며 할당 및 참조 동등에 대해 서로 다른 규칙이 있습니다.
그것들에 대해 이야기해 봅시다.
자바스크립트의 원시 타입
string, number, boolean(또한 symbol, undefined 및 null)과 같은 기본 타입이 있습니다.
이것들은 불변입니다. (a.k.a. 읽기 전용)
즉 값을 변경할 수 없습니다.
변수가 이러한 기본 타입 중 하나를 가리키고 있으면, 값을 수정할 수는 없습니다.
아예 새로운 값을 가리키도록 할 수만 있습니다.
즉, 해당 변수만 새 값으로 재할당할 수 있습니다.
즉 아래와 같이 동작하지 않습니다.
이것이 예를 들어 문자열의 모든 메서드가 문자열을 수정하는 대신 새 문자열을 반환하는 이유이며,
새 값을 원하면 어딘가에 저장해야 합니다.
let name = "Dave"
name.toLowerCase();
console.log(name) // still capital-D "Dave"
name = name.toLowerCase()
console.log(name) // now it's "dave"
기타 모든 타입: 객체, 배열 등
다른 범주는 객체 타입입니다.
여기에는 객체, 배열, 함수 및 Map 및 Set과 같은 기타 데이터 구조가 포함됩니다.
그것들은 모두 객체입니다.
기본 유형과의 가장 큰 차이점은 객체가 변경 가능하다는 것입니다!
즉, 상자에서 값을 변경할 수 있습니다.
불변은 예측가능합니다.
요약: 변수는 상자를 가리키는 태그입니다. 기본 타입은 변경할 수 없습니다.
우리는 변수를 할당하거나 재할당하는 것이 변수라는 이름표가 값을 포함하는 "상자를 가리키는" 방법임을 이야기했습니다.
그리고 리터럴 값(변수와 반대)을 할당하면 새 상자가 생성되고 변수가 해당 상자를 가리킵니다.
let num = 42
let name = "Dave"
let yes = true
let no = false
let person = {
firstName: "Dave",
lastName: "Ceddia"
}
let numbers = [4, 8, 12, 37]
박스 안의 내용물을 변경하기
우리는 대출할 수 있는 도서관의 book을 나타내는 book 객체로 시작할 것입니다.
title과 author 및 isCheckedOut 플래그가 있습니다.
let book = {
title: "Tiny Habits",
author: "BJ Fogg",
isCheckedOut: false
}
book.isCheckedOut = true
book 변수가 어떻게 변경되지 않는지 확인하십시오.
같은 객체를 들고 있는 같은 상자를 가키립니다.
변경된 것은 해당 객체의 속성 중 하나일 뿐입니다.
이것이 어떻게 이전과 동일한 규칙을 따르는지 주목하세요.
유일한 차이점은 변수가 이제 객체 내부에 있다는 것입니다.
최상위 isCheckedOut 변수 대신 book.isCheckedOut으로 액세스하지만
재할당하는 것은 똑같은 방식으로 작동합니다.
이해해야 할 중요한 것은 객체가 변경되지 않았다는 것입니다.
사실, book을 수정하기 전에 다른 변수에 저장하여 책의 "사본"을 만들더라도
여전히 새 객체를 만들지 않을 것입니다.
let book = {
title: "Tiny Habits",
author: "BJ Fogg",
isCheckedOut: false
}
let backup = book
book.isCheckedOut = true
console.log(backup === book) // true!
console.log(backup.isCheckedOut) // also true!!
마지막에 있는 console.log는 요점을 추가로 증명합니다.
book은 동일한 객체를 가리키고 있고,
book의 속성을 수정해도 객체의 껍데기가 변경되지 않고 내부만 변경되었기 때문에
여전히 backup과 동일합니다.
이것은 좋은 점입니다.
모든 변수가 독립적이며 변수가 다른 변수를 가리키는 거대한 지도를 머리 속에 보관할 필요가 없다는 것을 의미합니다.
만약 그렇다면 추적하기가 매우 어려울 것입니다!
함수 안에서 객체 변경하기
즉, 이미 book.isCheckedOut 또는 house.address.city와 같은 하위 속성이 아닌
book 또는 house와 같은 최상위 변수인 한 함수 내부의 변수 재할당이 외부에 누출되지 않음을 이야기했습니다.
function doesNotLeak(word) {
// this assignment does not leak out
word = "world"
}
let test = "hello"
reassignFail(test)
console.log(test) // prints "hello"
function checkoutBook(book) {
// this change will leak out!
book.isCheckedOut = true
}
let book = {
title: "Tiny Habits",
author: "BJ Fogg",
isCheckedOut: false
}
checkoutBook(book);
그러면 다음과 같은 일이 발생합니다.
function pureCheckoutBook(book) {
let copy = { ...book }
// this change will only affect the copy
copy.isCheckedOut = true
// gotta return it, otherwise the change will be lost
return copy
}
let book = {
title: "Tiny Habits",
author: "BJ Fogg",
isCheckedOut: false
}
// This function returns a new book,
// instead of modifying the existing one,
// so replace `book` with the new checked-out one
book = pureCheckoutBook(book);
이와 같이 변경할 수 없는 함수를 작성하는 방법에 대해 자세히 알아보려면 불변성에 대한 제 가이드를 읽어보세요.
(guide to immutability)
React와 Redux를 염두에 두고 작성되었지만 대부분의 예제는 일반 JavaScript입니다.
주 : 걍 immer쓰세요
실제 세계 사례
참조에 대한 새로운 지식을 바탕으로 문제를 일으킬 수 있는 몇 가지 예를 살펴보겠습니다.
솔루션을 읽기 전에 문제를 발견할 수 있는지 확인하십시오.
DOM 이벤트 리스너
- 이벤트 리스너를 추가하려면 이벤트 이름과 함수를 사용하여 addEventListener를 호출하세요.
- 이벤트 리스너를 제거하려면 추가한 함수와 동일한 함수 참조와 같이 동일한 이벤트 이름과 동일한 함수로 removeEventListener를 호출하세요.
- (그렇지 않으면 이벤트에 여러 함수가 추가될 수 있으므로 브라우저는 제거할 함수를 알 수 없습니다)
이 코드를 살펴보십시오. 추가/제거 기능을 올바르게 사용하고 있습니까?
document.addEventListener('click', () => console.log('clicked'));
document.removeEventListener('click', () => console.log('clicked'));
두 개의 화살표 함수가 참조적으로 동일하지 않기 때문에 이 코드는 이벤트 리스너를 제거하지 않습니다.
구문에 관한 한 동일하더라도 동일한 함수(참조)는 아닙니다.
화살표 함수() => { ... } 또는 일반 함수 함수 things() { ... }를 작성할 때마다 새 객체가 생성됩니다.
(함수는 객체임을 기억하십시오).
let a = () => {}
let b = () => {}
console.log(a === b)
const onClick = () => console.log('clicked');
document.addEventListener('click', onClick);
document.removeEventListener('click', onClick);
의도하지 않은 변경
function minimum(array) {
array.sort();
return array[0]
}
const items = [7, 1, 9, 4];
const min = minimum(items);
console.log(min)
console.log(items)
배열의 .sort() 메서드는 배열을 제자리에 정렬합니다.(sorts the array in place)
즉, 원래 배열의 순서를 변경합니다.
function minimum(array) {
const newArray = [...array].sort();
return newArray[0]
}
결론
- 변수는 이름표입니다.
- 변수는 값을 담은 상자가 아닙니다.
- 변수는 변수를 가리키지 않습니다.
- 값은 상자에 담겨있습니다.
- 변수는 상자만 가리킵니다.
- 변수에 변수 할당은 다른 변수가 가리키는 상자를 가리키게 하는 것입니다.
- 즉 변수는 반드시 1단계만에 상자에 도달합니다. 중간단계는 없습니다.
- 값은 primitive 타입일 경우 불변입니다.
- 값은 reference 타입일 경우 가변입니다.
- reference 타입은 내부에 primitive 타입을 가질 수 있습니다.
- 변수 재할당은 가리키는 상자를 바꾸는 것입니다.
- 불변 값의 경우 항상 상자가 바뀝니다.
- 가변 값의 경우 포장지가 바뀌지 않으면 상자가 바뀌지는 않습니다.
'FrontEnd' 카테고리의 다른 글
디자인시스템 토큰 이름붙이기 [Naming Tokens in Design Systems] (0) | 2022.10.05 |
---|---|
Vue.js 내부 들여다보기[Demystifying Vue.js internals] (0) | 2022.10.05 |
[React hooks] 리액트 훅의 원리 : 단지 배열일 뿐 (0) | 2022.10.03 |
리덕스 이후의 삶(Life after Redux) : 리덕스 없이 리덕스 장점 누리기 (0) | 2022.10.03 |
리액트 엘리먼트에 $$typeof 속성이 존재하는 이유 (0) | 2022.10.01 |