원시형 데이터는 한번 생성되면 값으로서 변경할 수 없습니다.
원시 값은 변경 불가능한 값이기 때문에 값을 직접 변경할 수 없습니다.
따라서 변수 값을 변경하기 위해 원시 값을 재할당하면 새로운 메모리 공간을 확보하고 재할당한 값을 저장한 후 변수가 참조하던 메모리 공간의 주소를 변경합니다.
이러한 특성을 불변성이라고 합니다.
let age = 50;
let copy = age;
console.log(age === copy)
//true
age = 100; //값 재할당
console.log(age === copy)
//false
console.log(age, copy)
//100, 50
왜 true가 출력될까?
식별자 age와 식별자 copy는 각각 다른 메모리 공간 즉, 다른 메모리 주소를 가지고 있습니다. 하지만 50과 50을 비교하면 같은 숫자형이며 50이기 때문에 true가 출력됩니다.
왜 재할당 후 age와 copy를 비교했을 때 false가 출력될까?
원시형 데이터는 불변하기 때문에 원래 변수 age가 가지고 있던 50이 저장된 메모리 공간에 새로운 값인 100 값으로 변경할 수 없습니다.
따라서 새로운 메모리 공간에, age라는 식별자와 연결된 새로운 메모리 주소에 100을 저장합니다.
하지만 copy는 여전히 50인 값이 담긴 메모리 공간, 메모리 주소를 가지고 있습니다. 따라서 false가 출력됩니다.
결국
두 변수의 원시 값은 서로 다른 메모리 공간에 저장된 별개의 값이 되어 어느 한쪽에서 재할당을 통해 값을 변경하더라도 서로 간섭할 수 없게 됩니다.
불변성을 갖는 원시 값을 할당한 변수는 재할당 이외에 변수 값을 변경할 수 있는 방법은 없습니다.
객체는 프로퍼티의 개수가 정해져 있지 않으며, 동적으로 추가되고 삭제할 수 있습니다.
따라서 원시 값처럼 확보해야 할 메모리 공간의 크기를 사전에 정할 수 없습니다.
참조형 데이터는 (이하 객체)는 변경 가능한 값입니다.
객체를 할당한 변수가 기억하는 메모리 주소를 통해 메모리 공간에 접근하면 참조 값에 접근할 수 있습니다. 참조 값은 생성된 객체가 저장된 메모리 공간의 주소, 그 자체입니다.
따라서 객체를 할당한 변수는 재할당 없이 객체를 직접 변경할 수 있습니다.
즉, 재할당 없이 프로퍼티를 동적으로 추가할 수도 있고 프로퍼티 값을 갱신할 수도 있으며 프로퍼티 자체를 삭제할 수도 있습니다.
하지만 객체는 여러 개의 식별자가 하나의 객체를 공유합니다. 이것은 구조적인 단점에 따른 부작용이 됩니다.
const joah = { name: 'Eunkyeong Kim' };
const copiedJoah = joah;
console.log(copiedJoah === joah); //true
console.log(copiedJoah.name === joah.name) //true
console.log(copiedJoah.name, joah.name) //Eunkyeong Kim, Eunkyeong Kim
copiedJoah.name = "bumsoo";
//재할당
console.log(copiedJoah === joah); //true
console.log(copiedJoah.name === joah.name) //true
console.log(copiedJoah.name, joah.name) //bumsoo, bumsoo
왜 true가 출력될까요?
변수 joah는 메모리 주소가 할당되어 있습니다. 그 메모리 주소를 따라가면 객체 {name : 'Eunkyeong Kim}
이 저장되어 있습니다.
변수 copiedJoah에는 joah가 할당되어 있습니다. 따라서 copiedJoah와 joah는 같은 메모리 주소를 바라보고 있습니다.
copiedJoah 재할당 했을 때 왜 joah의 값도 bumsoo로 바뀌었나요?
객체는 참조형 데이터이므로 원시형과는 다르게 가변합니다. 따라서 copiedJoah의 name 속성의 값을 bumsoo로 바뀌면 하나의 객체를 바라보는 copiedJoah와 joah 둘다 값이 변경되는 겁니다.
이렇게 되면 원본이 수정되는 치명적인 일이 발생하네요 그래서 불변성을 유지하기 위해서는 어떻게 할 수 있죠?
참조형 데이터의 불변성을 부여하기 위해서는 2가지 방법이 있습니다.
얕은 복사는 한 단계까지만 복사하는 것을 말합니다.
얕은 복사는 객체에 중첩되어 있는 객체의 경우 참조 값을 복사합니다.
스프레드 연산자를 사용합니다.
const joah = { name: { first: "Eunkyeong", last: "Kim" } };
const shallowCopiedJoah = {...joah};
console.log(shallowCopiedJoah === joah); //false
console.log(shallowCopiedJoah.name.first === joah.name.first); //true
console.log("재할당 전 원본", joah.name.first); //Eunkyeong
console.log("재할당 전 복사본", shallowCopiedJoah.name.first); //Eunkyeong
shallowCopiedJoah.name.first = "bumsoo";
console.log(shallowCopiedJoah === joah); //false
console.log(shallowCopiedJoah.name.first === joah.name.first); //true
console.log("재할당 후 원본", joah.name.first); //bumsoo
console.log("재할당 후 복사본", shallowCopiedJoah.name.first); //bumsoo
왜 false가 출력되나요?
스프레드 연산자를 사용하여 얕은 복사를 하면 서로 다른 메모리 주소값을 가지게 됩니다. 따라서
console.log(shallowCopiedJoah === joah); //false
결과를 가져오죠! 복사가 되었다는 겁니다.
얕은 복사는 다른 메모리에 저장되어 원본 사본이 있는데 재할당을 shallowCopiedJoah.name
에만 했는데 왜 joah.name
도 수정된 값 bumsoo
가 출력되나요?
joah와 shallowCopiedJoah의 메모리 주소는 다르지만 동일한 참조 값을 갖습니다. 다시 말 해, 원본과 복사본이 모두 동일한 객체를 가리킵니다.
즉, joah에는 참조값인 객체 데이터가 저장된 메모리 주소가 할당되어 있고 같은 주소가shallowCopiedJoah에도 동일하게 할당되어 있습니다.
두개의 식별자가 하나의 객체를 공유하고 있습니다.
따라서 원본 또는 사본 중 어느 한쪽에서 객체를 변경하면 서로 영향을 주고 받습니다.
그럼 중첩된 객체를 복사하고 따로 값을 할당하고 싶을 때는 어떻게 하면 될까요?
이때는 깊은 복사를 활용합니다. 깊은 복사는 변수를 또 생성하고 각각의 객체를 다시 할당하는 번거로움이 있어 보통은 모듈이나 라이브러리(immer)를 사용합니다.
const joah = { name: { first: "Eunkyeong", last: "Kim" } };
const _ = require("lodash"); //npm install lodash 필요
const deepCopiedJoah = _.cloneDeep(joah);
//lodash의 cloneDeep메소드 활용
console.log(deepCopiedJoah === joah); //false
console.log(deepCopiedJoah.name.first === joah.name.first); //true
console.log("재할당 전 원본", joah.name.first); //Eunkyeong
console.log("재할당 전 복사본", deepCopiedJoah.name.first); //Eunkyeong
deepCopiedJoah.name.first = "bumsoo";
console.log(deepCopiedJoah === joah); //false
console.log(deepCopiedJoah.name.first === joah.name.first); //false
console.log("재할당 후 원본", joah.name.first); //Eunkyeong
console.log("재할당 후 복사본", deepCopiedJoah.name.first); //bumsoo
깊은 복사는 중첩되어 있는 객체까지 모두 복사해서 원시 값처럼 완전한 복사본을 만듭니다.
즉, joah와 deepCopiedJoah는 완전히 다른 객체가 됩니다. 서로 같은 주소값을 참조하지 않습니다.
따라서 deepCopiedJoah.name.first
에 “bumsoo”를 재할당해도 원본인 joah.name.first
의 값은 변하지 않습니다. 마치 원시형 데이터의 불변성과 같죠?
리액트의 일반적인 데이터 흐름은 부모에서 자식으로 props를 넘겨주는 형태입니다.
만약 A라는 최상위 컴포넌트에서 E에게 전달할 데이터가 있다면, E의 부모 컴포넌트인 A<B<C<D를 거쳐야 E에게 A의 데이터가 도달할 수 있습니다.
그럼 A,B,C,D는 해당 데이터가 필요하지도 않은데 전달 받아야 하는 비효율성이 있으며, 데이터 양이 방대하고 복잡한 로직이라면 작은 실수라도 모래에서 바늘 찾기가 될 것입니다.
즉, 전역 상태를 자식 컴포넌트에게 효율적으로 전달하기 위해서 필요한 것이 context API 입니다.
리덕스나 MobX 같은 외부 라이브러리를 사용할 수 있지만 리액트 v.16.3 업데이트 이후 Context API가 많이 개선되었기 때문에 별도의 라이브러리를 사용하지 않아도 전역 상태를 손쉽게 관리할 수 있습니다. 보통은 user의 정보, Theme, Language 상태를 전달하기 위해 사용합니다.
사용법
context 파일을 생성한 후 최상위 폴더의 return문에 import한 context 파일을 컴포넌트로 작성합니다. 이때 모든 자식 컴포넌트를 감쌀 수 있게 작성하면 됩니다.
이후 전역 상태가 필요한 자식 컴포넌트는 useContext Hook을 활용하여 불러오기만 하면 됩니다.
주의할 점
context를 사용하면 컴포넌트의 재사용이 어려워질 수 있기 때문에 꼭 필요할 때 사용하길 권장합니다.
특징
개념은 context API와 동일합니다. 왜 사용하는지, 어떤 개념인지는 같은 맥락입니다.
Redux는 디자인 패턴인 Flux를 기반으로 생성된 구현체 입니다.
단방향 데이터 흐름을 이용해 예측가능하고 일괄적인 상태 컨테이너의 역할을 제공하는 라이브러리 입니다.
Redux의 구조
Redux의 데이터 흐름을 보면
view에서 event가 발생하면 Action 객체를 생성합니다.
이 객체를 Dispatch로 전달하며 중간에 middleWare가 Reducer에게 전달합니다. Store는 상태를 관리하는 곳인데 이곳에서 state를 변경합니다. state가 변경되면 UI에 반영이 됩니다.
특징
웹페이지의 디자인이 웹브라우저의 기본 디자인과 브라우저 사용자의 디자인 그리고 웹페이지 저자의 디자인이 결합될 수 있다는 점에 착안하고 있다고 할 수 있을 것 같습니다.
사실 웹브라우저<사용자<저자 순서대로 개발자가 지정하는 스타일링이 우선하는 것은 맞습니다. 미래에는 사용하는 사용자가 마음대로 스타일링을 할 수 있도록 하는 것이 궁극적인 목표가 될 것 같네요!
웹브라우저, 사용자, 컨텐츠 생산자의 조화를 이루기 위해서는 cascading을 위해서는 규칙이 필요합니다. 이 규칙은 우선순위를 정합니다.
하나의 태그에 여러 스타일링이 겹친다면
의 순서대로 우선순위를 갖습니다. CSS를 작성할 때 반드시 고려해야 하는 부분이며 무분별한 !important 사용은 지양해야 합니다.
간단하게 처리하는 애니메이션의 경우 CSS로 처리 합니다. transform
/ translate
를 사용합니다.
브라우저 렌더링 과정에서 layout 이나 paint 단계를 거쳐야 할 경우가 생길 수 있기 때문에 성능 개선에 효율적이지 않을 수 있습니다.
특징
CSS로 처리하기에는 훨씬 복잡하고 무거운 애니메이션 작업들을 효율적이고, 세밀하게 다루기 위해 사용합니다. 또한 외부 라이브러리들로 하여금 성능 좋은 애니메이션을 구현 할 수 있습니다.
CSS 애니메이션은 낮은 버전의 브라우저에서는 지원을 하지 않는 경우가 있습니다.(특히, IE)