이 부분을 건너뛰고 공부하려고 했는데, setEffect가 side-effect를 유발하기 위한 Hooks이라는 말을 듣고 의문이 들었다.
리액트는 side-effect를 방지하는 것이 원칙이라면서 이런 기능을 제공한다고?
그래서 불변성에 대한 개념부터 차근차근 이해해보려 한다.
목차
1. What is Immutability(불변성)?
2. What is Side-Effect(부수효과)?
3. useEffect
4. 참고 자료
1. What is Immutability?
"불변성"은 값이나 상태를 변경할 수 없는 것을 의미하고,
"불변성을 지킨다"는 결국 기존의 값을 직접 수정하지 않으면서 새로운 값을 만들어 내는 것을 일컫는다.
대체 이게 무슨 소리냐?
원시 타입과 객체/배열 타입의 경우를 나누어 생각해보자.
let a = 10;
let b = a;
a = 20;
console.log(a, b); // 20 10
JavaScript에서 원시타입은 값을 변경할 때, 불변성을 알아서 잘 지킨다.
만약 그렇지 않다면 a에 20이라는 값을 할당했을 때, 10이 제거되고 20이라는 값으로 바뀔 것이다.
즉, b의 값도 20이 되었어야 맞는데 정작 결과는 a는 20이고 b는 10이다.
굳이 b라는 변수를 만든 이유는 a만 놓고보면 조금 헷갈리기 때문이었는데 a만 있다고 가정해보자.
let a = 10
a = 20
이건 과연 a의 값이 10 → 20으로 변경되었음을 의미할까? 아니다, 메모리 영역에는 10과 20 둘 다 존재한다.
이것이 불변성이다. 원본을 건드리지 않고 새로운 값을 저장한 후, 값이 들어있는 주소를 변수에게 던져준 것이다.
자바 스크립트에서 Boolean, Number, String, null, undefined, Symbol과 같은 타입들은 불변성을 지킨다.
(메모리 구조를 C로 팠었더니 String이 불변성을 지킨다는 게 왜이렇게 생소할까 ㅋㅋㅋ)
문제는 Object의 경우에는 이야기가 조금 달라진다.
let a = { value: 10 }
let b = a;
a.value = 20
console.log(b.value) // 20
console.log(a === b) // true
이 경우에 a를 b에 할당하면 데이터의 주소를 똑같이 참조하고 있게 된다. 이게 그 유명한 얕은 복사를 의미한다.
그래서 난 분명 a의 값만 수정하고 싶었는데 불구하고 b의 객체의 값까지 달라지는 의도치 못한 결과를 낳는다.
(사실 a의 객체, b의 객체로 나누는 게 우스운 일이다. 둘다 같은 객체의 메모리 주소를 가지고 있으니까.)
이렇게 되면 디버깅이 너무 힘들어 진다.
그래서 객체의 경우에는 spread 연산자를 사용하여 깊은 복사를 통해 불변성을 지킨다.
그렇다면 불변성을 왜 지켜주어야 할까?
💡 React가 Update를 하기 위해서는 객체가 새로 생성되는 정보를 통해 변화를 인지해야 한다.
리액트는 얕은 비교 수행(복사가 아님)을 통해 업데이트해야 할 내역을 갱신한다.
즉, Object 속을 다 까보는 것이 아니라 참조값만 비교해서 전에 없던 객체가 생성됨을 인지함으로써 업데이트를 할 지 말 지 판단하는 것이다.
그렇기 때문에 원본 객체의 값을 직접적으로 변경하면 리액트는 이 변화를 인지하지 못 한다.
결국 불변성을 지켜주어야 하는 것은 효율적으로 앱의 상태를 업데이트하고 사이드 이펙트를 방지하기 위함이다.
그런데 아까부터 자연스레 등장하는 Side-Effect란 대체 뭔데?
2. What is Side-Effect?
함수가 실행되면서 함수 외부의 값, 상태를 변경시키는 것을 말한다.
(input-output 이외에 함수 외부의 값을 조작하는 것.)
함수가 전역변수의 값을 바꾸거나, 쿠키를 저장하고, 데이터를 송신하는 것도 포함된다.
하지만 언어 공부를 할 때만 생각해도 지금 설명하는 Side-Effect는 너무 흔히 사용하던 방법이다.
문제는 규모가 커짐에 따라 Side-Effect가 자꾸 예상치 못 한 결과를 발생시키고, 심지어 그 문제 원인을 찾는 과정도 힘들어졌기 때문에 최근 선언형 프로그래밍에서는 이를 최소화하는 방향을 지향하고 있다.
한 가지 예를 들어 보자.
내가 궁금했던 사항에 대해 너무 자세히 설명해주고 계신 갓구름님..
function UserProfile({ name }) {
const message = `${name}님, 환영합니다.`;
document.title = `${name}`;
return <div>{message}</div>
}
현재 함수형 컴포넌드가 document로 부터 가져온 외부데이터를 수정하고 있다.
분명 지금 당장은 이 side-effect가 어떤 식으로 작동할지 100% 예측할 수 있다.
문제는 이런 코드가 2개, 3개로 늘다가 100개에 도달했다고 가정하자. 그래도 100%라고 장담할 수 있는가?
만약, 내가 짠 코드에 대해 자신있다고 하더라도 다른 사람이 이렇게 코드를 작성했다면 유지·보수가 쉬울까?
그렇지 않다.
리액트의 작동 원리를 이해하면 또 한 가지 문제점이 드러난다.
Render phase 단계에선 props와 state를 바탕으로 VDOM을 그린다.
아직 화면에 그리는 게 아니라 말 그대로 화면에 출력할 내용의 좌표, 크기 등을 파악하는 과정이다.
Render Tree과 완성되면 그제서야 commit Phase에서 그려진 VDOM을 바탕으로 RealDOM에 마운트 한다.
(여기까지도 아직 화면에 그리는 단계가 아니다.)
과정은 여기에 간략히 정리해두었으니 필요하면 참고하자.
정리하자면 외부에서 데이터를 받아오면 그것을 기반으로 컴포넌트 생성의 초기값을 설정해 VDOM을 생성하고,
RealDOM에 마운트하는 과정을 거쳐, 변경된 외부 데이터 변화를 통해 다시 render, commit 단계를 거쳐 데이터를 갱신하는 과정을 수행하고 있다. (설명만 들어도 복잡하다.)
이럴 거면 그냥 render phase 단계에 들어서기 전에 처음부터 데이터를 받아오면 되지 않겠냐는 의문이 생긴다.
그런데 주먹구구식 해결방법은 언제나 성능 저하를 일으킨다.
VDOM을 그리는 과정은 순수한 부분만이 포함되어야 이전과 현재의 렌더링 결과를 비교하여 Update할 수 있다.
그런데 Side-Effect가 VDOM을 그리는 과정에서 발생해버리면, 부수 효과 발생 시마다 VDOM을 다시 그려야 한다.
심지어 데이터 요청을 해야하는 경우에는 동기적으로 네트워크 응답을 받아야 다음 Phase로 넘어갈 수 있다.
여기까지 이해했으면 Side-Effect가 굉장히 다루기 힘들고, 리스크가 크다는 것을 알고 기피하게 될 수도 있다.
하지만, 웹 어플리케이션에서 Side-Effect는 필수적으로 존재해야 한다.
이런 현실과 이상의 괴리(?)의 간극을 좁히기 위해 나온 것이 바로 useEffect다.
그렇다면 대체 useEffect는 뭐길래 부수 효과를 다룰 수 있다는 걸까?
3. useEffect
결론부터 말하자면 문제의 원인은 결국 순수한 DOM이 필요하다는 것에서 출발한다.
그렇다면 render phase 단계에 들어서기 전에 처음부터 데이터를 받아오긴 하되,
순수한 DOM으로 일단 화면을 다 그리고 마지막에 Side-Effect를 발생시켜버린다는 것이 useEffect다.
이렇게 하면 컴포넌트가 갱신되는 도중에 다시 갱신시켜야 하는 과정이 없기 때문에 성능에 영향을 주지 않는다.
해구름님의 코드를 useEffect를 사용한 버전으로 다시 확인해보자.
function UserProfile({ name }) {
const message = `${name}님 환영합니다!`;
//Side-Effect 코드를 UseEffect로 분리
useEffect(() => {
document.title = `${name}의 개인정보`;
}, [name]);
return <div>{message}</div>;
}
이렇게 하면, 리액트는 Side-Effect를 최적의 조건에서 실행시킬 수 있다.
또한 어느 개발자가 봐도 이 코드가 side-effect를 일으키기 위함을 인지할 수 있기 때문에 유지·보수에도 용이하다.
하지만 그럼에도 useEffect을 남발해서는 안 된다.
useEffect가 N개 있을 때, 고려해야할 경우의 수는 선형적으로 증가하지 않는다. 기하급수적으로 늘어날 수도 있다.
그렇게 되면 side-effect의 흐름을 추적하기도 어렵고 아무리 리액트가 최적화를 시키려 해도 한계가 있을 수 있다.
따라서 useEffect를 사용할 때는 이런 점들을 참고하고 코드를 작성하도록 해야한다.
4. 참고 자료