11. 불변성 & 순수함수
11.1 불변성
1. 불변성이란?
불변성이란 메모리에 있는 값을 변경할 수 없는 것을 말한다. 원시데이터는 불변성이 있고, 원시 데이터가 아닌 객체, 배열, 함수는 불변성이 없다.
2 .변수를 저장하면, 메모리에 어떻게 저장이 될까?
만약 우리가 let number = 1 이라고 선언을 하면, 메모리에는 1이라는 값이 저장되는데, number라는 변수는 메모리에 있는 1을 참조하게 된다. 다음 let secondNumber = 1 라는 또다른 변수를 선언했다고 가정했을 때 JS는 이미 메모리에 생성되어 있는 1이라는 값을 참조한다. 두 변수는 이름은 다르지만, 같은 메모리의 값을 바라보고 있다.
let number = 1;
let secondNumber = 1;
console.log(number === secondNumber) // true
하지만 원시데이터가 아닌 값(객체, 배열, 함수)는 이렇지 않다.
let obj_1 = {name:'yim'}이라는 값을 선언하면 메모리에 obj_1이 저장된다. 이어서 let obj_2 = {name:'yim'}이라고 같은 값을 선언하면 obj_2라는 메모리 공간에 새롭게 저장된다.
let obj_1 = {name : 'yim'};
let obj_2 = {name : 'yim'};
console.log(obj_1 === obj_2); // false
3. 데이터를 수정하면 어떻게 될까?
만약에 기존에 1이던 number를 number = 2 라고 새로운 값을 할당하면 어떻게 될까?
원시데이터는 불변성이 존재하며, 즉 기존 메모리에 있는 1이라는 값이 변하지 않고, 새로운 메모리 저장공간에 2가 생기게 된다.
number라는 변수는 새로운 메모리 공간에 저장된 2를 참조하게 되며, 여전히 secondNumber는 다른 공간의 1을 참조하고 있다.
원시데이터가 아닌 obj_1을 수정하게 되면 obj_1.name = 'kim'이라고 새로운 값을 할당하면 어떻게 될까?
객체는 불변성이 존재하지 않기 때문에, 기존 메모리 저장공간에 있는 {name: 'yim'}이라는 값이 {name: 'kim'}으로 바뀌어 버린다.
* 정리
원시데이터는 수정했을 때 메모리에 저장된 값 자체는 바꿀 수 없고, 새로운 메모리 저장공간에 새로운 값을 저장. 원시데이터가 아닌 데이터를 수정했을 때 기존에 저장되어 있던 메모리 저장공간의 값 자체를 바꿈.
4. 왜 리액트에서는 원시데이터가 아닌 데이터의 불변성을 지켜주는 것을 중요시 할까?
리액트에서는 화면을 리렌더링을 할지 말지 결정할 땐 state의 변화를 확인한다. 그때, state에 변화가 있는지 확인하는 방법이 state의 변화 전, 후의 메모리 주소를 비교한다.
만약 원시데이터가 아닌 데이터를 수정할 때 불변성을 지켜주지 않고, 직접 수정을 가하면 값은 바뀌지만 메모리 주소는 변함이 없게 된다.
즉, 개발자가 값은 바꿨지만 리액트는 state가 변한지 모르게 되기 때문에, 마땅히 일어나야 할 리렌더링이 일어나지 않게 된다.
5. 리액트 불변성 지키기 예시
배열을 setState 할 때 불변성을 지켜주기 위해, 직접 수정을 가하지 않고 전개 연산자를 사용해서 기존의 값을 복사하고, 그 이후에 값을 수정하는 식으로 구현한다.
import React, { useState } from "react";
function App() {
const [dogs, setDogs] = useState(["말티즈"]);
function onClickHandler() {
// spread operator(전개 연산자)를 이용해서 dogs를 복사합니다.
// 그리고 나서 항목을 추가합니다.
setDogs([...dogs, "시고르자브르종"]);
}
console.log(dogs);
return (
<div>
<div className="">{dogs}</div>
<button onClick={onClickHandler}>버튼</button>
</div>
);
}
export default App;
11.2 순수함수
개념
하나 이상의 인자를 받고, 인자를 변경하지 않고, 참조하여 새로운 값을 반환하는 함수이며, 같은 인자가 전달되면 항상 동일한 결과를 반환해야 한다.
(1) 순수함수
// 매개변수를 복사한 값을 변경하는 순수함수
const addSixPure = (arr) => {
// 펼침 연산자로 새로운 배열에 6 추가
newArr = [...arr, 6];
return newArr;
};
(2) 순수함수가 아닌 것
const num_arr = [1, 2, 3, 4, 5];
// 매개변수의 값을 직접 변경하는 불순함수
const addSixImpure = (arr) => {
// 매개변수에 직접 6 추가
arr.push(6);
return arr;
};
리액트에선 많은 루틴을 순수함수로 작성을 요구 하고 있다.
컴포넌트에서 state와 props가 같으면, 항상 같은 값을 반환해야하며, 다른 Side effects를 발생시키지 않아야한다.
또한 컴포넌트의 상태 값은 불변 객체(Immutable Object)로 관리해야만 하며, 수정할 때에는 기존 값을 변경하는 것이 아니라, 같은 이름의 새로운 객체를 생성해야 한다.
이를 통해, UI 개발의 복잡도를 낮추고, 버그 발생 확률도 줄여준다.
12. Component & Rendering
컴포넌트는 리액트의 핵심 빌딩 블록 중 하나로, UI 요소를 표현하는 최소한의 단위이며 화면의 특정 부분이 어떻게 생길지 정하는 선언체다.
컴포넌트기반 이전에는 브라우저에서 동적으로 변하는 UI를 표현하기 위해서 DOM 객체를 조작하는 명령형 프로그래밍 방식으로 구현 했는데, 리액트는 선언적 프로그래밍 방식으로 구현한다.
명령형 : 어떻게(How)를 중요시 여기며 프로그램의 제어의 흐름과 같은 방법을 제시하고, 목표를 명시하지 않는 형태
선언형 : 무엇(What)을 중요시 여기며 제어의 흐름보다는 원하는 목적을 중요시 여기는 형태
⭐DOM (명령형 프로그래밍)
명령형으로 작성된 코드는 컴퓨터가 수행하는 절차를 일일히 코드로 작성해주어야 한다.
// Hello, World! 화면에 출력하기
// 순수 javaScript 명령형 코드
const root = document.getElementById('root');
const header = document.createElement('h1');
const headerContent = document.createTextNode(
'Hello, World!'
);
header.appendChild(headerContent);
root.appendChild(header);
⭐리액트 (선언형 프로그래밍)
선언형으로 작성된 코드는 UI를 선언하고 render 함수를 호출하여 컴퓨터가 수행하는 절차를 리액트가 대신 수행한다.
// React 코드 (선언적인)
const header = <h1>Hello World</h1>; // jsx
ReactDOM.render(header, document.getElementById('root'));
2. Rendering
리액트에서 렌더링이란, 컴포넌트가 현재 props와 state의 상태에 기초하여 UI를 어떻게 구성할지 컴포넌트에게 요청하는 작업을 의미한다.
다음은 렌더링 프로세스를 설명하는 내용이다.
렌더링 트리거
컴포넌트가 렌더되는 이유는 2가지가 있다.
- 컴포넌트의 초기 렌더링
- 컴포넌트나 조상 컴포넌트의 상태가 업데이트
1. 초기 렌더링 : 앱이 시작되면 초기 렌더링을 트리거하는 것이 필요하며, 타깃 DOM 노드와 함께 createRoot를 호출하고, 컴포넌트와 함꼐 render 메서드를 호출한다.
import App from "./App.js";
import { createRoot } from "react-dom/client";
const root = createRoot(document.getElementById("root"));
root.render(<App />);
2. 리렌더링 : state가 업데이트 되면 발생하며, 렌더링 해야하는 부분을 큐에 입력 한다.
컴포넌트 렌더링
컴포넌트 렌더링이 트리거 된 후, 화면에 어떤 것이 보여야 할지 알아내기 위해 컴포넌트를 호출한다.
초기 렌더링 : root 컴포넌트를 호출
리렌더링 : 상태의 업데이트로 렌더링이 트리거 된 컴포넌트를 호출
DOM에 commit
컴포넌트를 렌더링 한 후 DOM을 수정한다.
초기 렌더링 : appendCild()를 사용하여 생성된 모든 DOM 노드를 화면에 표시
리렌더링 : 최근 렌더링 결과와 일치하도록 최소한의 필수 작업을 적용 (diffing 알고리즘)
브라우저 페인트
렌더링이 끝나면 리액트가 DOM을 업데이트하면 브라우저는 화면을 다시 브라우저를 렌더링하는데, 이를 브라우저 페인팅이라고 한다.
13. 실습 - 카운터 앱
import React, { useState } from "react";
function App() {
const [count, setCount] = useState(0);
const countUp = () => setCount(count + 1);
const countDown = () => setCount(count - 1);
return (
<div style={{ textAlign: "center" }}>
<div className="">{count}</div>
<button onClick={countUp}>+1</button>
<button onClick={countDown}>-1</button>
</div>
);
}
export default App;
14. Styling
생략
15. 반복되는 컴포넌트 처리하기
1. 리액트에서의 map
리액트에서는 반복되는 elements를 사용할 떄에 map()을 사용한다.
import React from "react";
const vegetables = ["감자", "고구마", "오이", "가지", "옥수수"];
return (
<div className="app-style">
{vegetables.map((vegetableName) => {
return (
<div className="square-style" key={vegetableName}>
{vegetableName}
</div>
);
})}
</div>
);
};
export default App;
2. key
리액트에서 컴포넌트를 반복 렌더링 할 때는 반드시 key를 넣어 주어야 한다.
key가 필요한 이유는 컴포넌트 배열을 렌더링했을 때 각각의 원소에서 변동이 있는지 알아내려고 사용하기 때문이다.
만약 key가 없다면 가상돔을 비교하는 과정에서 배열을 순차적으로 비교하여 변화를 감지하려 하지만 key가 있다면 어떤 변화가 일어났는지 더 빠르게 알아낼 수 있게 되어 성능이 더 최적화 된다.
map()의 index를 사용해서 key를 사용하는 것은 좋지 않은 방식이므로 지양해햐 한다.
16. 컴포넌트 분리하기
컴포넌트 분리가 필요한 이유
일반적으로, 계속 여러번 렌더링하여, 기능을 재사용하는 컴포넌트들은 따로 분리해서 사용한다.
하나의 폴더 안에 모든 컴포넌트를 만들어서 관리하면 시간이 흐를수록 컴포넌트가 많아져서 원하는 컴포넌트를 찾기가 힘들어 질 것이니, 연관된 컴포넌트끼리 폴더를 만들어서 관리하는 것이 컴포넌트를 찾기에 수월하다.
export와 export default 의 차이점
둘 다 모듈을 내보내고 불러오는 방법이며 크게 두 종류로 나뉜다.
- 여러개의 함수가 있는 라이브러리 형태의 모듈
- 개체 하나만 선언되어있는 모듈
대개는 두 번째 방식으로 모듈을 만드는 걸 선호하기 떄문에 함수, 클래스, 변수 등의 개체는 전용 모듈 안에 구현된다.
모듈은 export default라는 문법을 지원 하며, 이는 해당 모듈엔 개체가 하나만 있다는 사실을 명확히 나타낸다.
내보내고자 하는 개체 앞에 export default를 붙여 사용하면 된다.
export defaul를 사용했다면 import시에 중괄호 없이 모듈을 가져올 수 있다.
'항해99 플러스 > React 스터디' 카테고리의 다른 글
[React 스터디] 2주차 숙련 단계 (0) | 2025.02.17 |
---|---|
[React 스터디] 1주차 입문 단계 (1) | 2025.02.05 |