React - state
01. state
(1) state란 무엇일까요?
💡 State란 컴포넌트 내부에서 바뀔 수 있는 값을 의미합니다.
바뀌는 이유는 UI(엘리먼트)로의 반영을 위해서입니다.
(2) state 만들기
💡 State를 만들 때는 useState()를 사용한다.
import React, { useState } from 'react';
function GrandFather() {
const [name, setName] = useState("김할아"); // 이것이 state
return <Mother grandFatherName={name} />;
}
const name = “김할아” 라는 코드는 const [name, setName] = useState("김할아"); 라는 state로 작성할 수 있습니다.
우리는 앞으로 useState 라는 함수를 이용해서 state를 만듭니다. useState 는 state를 만들어주는 리액트에서 제공하는 기능입니다. 그래서 리액트에만 존재하는 개념이자 기능입니다. 그리고 앞으로 우리는 이것을 “기능” 이라고 하지 않고 “훅” 이라고 표현하겠습니다.
useState 훅을 사용하는 방식은 아래와 같습니다.
const [ value, setValue ] = useState( 초기값 ); // 배열의 구조분해 할당 사용
먼저 const 로 선언을 하고 [ ] 빈 배열을 생성하고, 배열의 첫 번째 자리에는 이 state의 이름, 그리고 두 번째 자리에는 set 을 붙이고 state의 이름을 붙입니다. 그리고 useState( ) 의 인자에는 이 state의 원하는 처음값을 넣어줍니다.
const [name, setName] = useState("김할아");
저는 name 이라는 state를 만들었고, name state의 처음값은 “김할아”로 정했습니다. 우리는 처음값을 initial state 라고 부릅니다. state의 정의처럼, 언제든지 변할 수 있는 값이기 때문에 처음값이라는 개념이 존재하는 것입니다.
(3) state 변경하기
💡 state를 변경할 때는 setValue(바꾸고 싶은 값) 를 사용한다.
// src/App.js
import React, { useState } from "react";
function Child(props) {
return (
<div>
<button
onClick={() => {
props.setName("박할아"); // 받은 setName을 실행합니다.
}}
>
할아버지 이름 바꾸기
</button>
<div>{props.grandFatherName}</div>
</div>
);
}
function Mother(props) {
return (
<Child grandFatherName={props.grandFatherName} setName={props.setName} /> // 받아서 다시 주고
);
}
function GrandFather() {
const [name, setName] = useState("김할아");
return <Mother grandFatherName={name} setName={setName} />; // 주고
}
function App() {
return <GrandFather />;
}
export default App;
하지만 이렇게 바뀐 값은 브라우저를 새로고침 하면 다시 초기값으로 바뀝니다. setName을 통해서 바꾼 값은 어디에 저장되는 것이 아니기 때문에 단순히 화면에서만 바뀐 값으로 다시 렌더링이 되는 것입니다.
02. state 기본 응용
(1) useState + onClick event
(1)-1. state 구현하고 이벤트 핸들러와 연결하기
우리가 어떤 버튼을 만들고 그것을 클릭했을 때 state를 변경하는 것을 만들어 보겠습니다.
우선 state를 하나 만듭니다. 그리고 이 버튼을 클릭을 했을 때 state 값을 바꿔보겠습니다. 이벤트 핸들러를 만들어주고 그 안에 setName 을 넣어줍니다.
버튼을 누르면 setName()안에 있는 값이 “누렁이”니까, state가 “길동이"에서 “누렁이”로 바뀝니다.
import React, { useState } from "react";
function App() {
const [name, setName] = useState("길동이");
function onClickHandler() {
setName("누렁이");
}
return (
<div>
{name}
<button onClick={onClickHandler}>버튼</button>
</div>
);
}
export default App;
(2) useState + onChange event
(1) input과 이벤트 핸들러를 구현하고 state와 연결하기
input에서는 보통 사용자가 입력한 값을 state로 관리하는 패턴을 많이 사용합니다.
먼저 input에 onChange라는 이벤트를 불러내고, 우리가 생성한 이벤트 핸들러 함수를 넣습니다.
import React, { useState } from "react";
const App = () => {
const [value, setValue] = useState("");
const onChangeHandler = (event) => {
const inputValue = event.target.value;
setValue(inputValue);
};
return (
<div>
<input type="text" onChange={onChangeHandler} value={value} />
</div>
);
};
export default App;
그리고 우리는 이벤트 핸들러 안에서 자바스크립트의 event 객체를 꺼내 사용할 수 있습니다. 사용자가 입력한 input의 값은 event.target.value 로 꺼내 사용할 수 있죠. 마지막으로 state인 value를 input의 attribute인 value에 넣어주면 input과 state 연결이 됩니다.
03. 불변성
(1) 불변성이란?(+메모리에 저장되는 원리)
(1)-1. 불변성이란?
불변성이란 메모리에 있는 값을 변경할 수 없는 것을 말합니다. 자바스크립트의 데이터 형태 중에 원시 데이터는 불변성이 있고, 원시 데이터가 아닌 객체, 배열, 함수는 불변성 없습니다.
*변경 가능한 방법(Mutable way) :
let numbers = [1, 2, 3];
numbers.push(4); // 배열에 직접 요소를 추가
console.log(numbers); // [1, 2, 3, 4]
위와 같은 방식이 왜 문제가 될까요? numbers.push(4)를 사용하는 방식이 문제가 되는 이유는 이 작업이 배열 numbers의 불변성을 깨뜨리기 때문입니다. 불변성을 깨뜨린다는 것은 원래의 데이터 구조를 직접 변경한다는 의미입니다. 이 경우에는 원본 배열 numbers에 직접적으로 새로운 요소 '4'를 추가하고 있습니다.
불변성을 깨뜨리는 것이 왜 문제가 될까요?
- 예측 불가능한 코드: 데이터 구조를 직접 변경하면, 프로그램의 다른 부분에서 해당 데이터 구조를 참조하고 있을 때 그 부분들도 예상치 못한 방식으로 변경될 수 있습니다. 따라서 알 수 없는 버그를 발생시킬 수 있고, 프로그램의 동작을 예측하기 어렵게 만듭니다.
- 버그 추적의 어려움: 원본 데이터가 여러 곳에서 변경될 수 있다면, 어떤 부분의 코드가 데이터를 변경했는지 추적하기 어려워집니다. 이는 특히 큰 코드베이스나 여러 개발자가 협업하는 환경에서 문제가 될 수 있습니다.
*불변성을 유지하는 방법(Immutable way) :
let numbers = [1, 2, 3];
let newNumbers = [...numbers, 4]; // 새 배열을 생성하여 기존 배열을 변경하지 않음
console.log(numbers); // [1, 2, 3]
console.log(newNumbers); // [1, 2, 3, 4]
(1)-2. 변수를 저장하면 메모리에 어떻게 저장이 될까?
만약 우리가 let number = 1 이라고 선언을 하면, 메모리에는 1 이라는 값이 저장됩니다. 그리고 number 라는 변수는 메모리에 있는 1을 참조하죠. 그리고 이어서 우리가 let secondNumber = 1 이라고 다른 변수를 선언을 했다고 가정해 봅시다. 이때도 자바스크립트는 이미 메모리에 생성되어 있는 1이라는 값을 참조합니다. 즉, number와 secondNumber는 변수의 이름은 다르지만, 같은 메모리의 값을 바라보고 있는 것이죠. 그래서 우리가 콘솔에 number === secondNumber 를 하면 true가 보입니다.
하지만 원시데이터가 아닌 값(객체, 배열, 함수)는 이렇지 않아요. let obj_1 = {name: ‘kim’} 이라는 값을 선언하면 메모리에 obj_1이 저장이 됩니다. 그리고 이어서 let obj_2 = {name: ‘kim’} 이라고 같은 값을 선언하면 obj_2라는 메모리 공간에 새롭게 저장이 됩니다. 그래서 obj_1 === obj2 는 false 가 되죠.
(1)-3. 데이터를 수정하면 어떻게 될까?
다시 원시데이터로 돌아와서 만약에 기존에 1이던 number를 number = 2 라고 새로운 값을 할당하면 메모리에서는 어떻게 될까요? 원시 데이터는 불변성이 있습니다. 즉, 기존 메모리에 저장이 되어 있는 1이라는 값이 변하지 않고, 새로운 메모리 저장공간에 2가 생기고 number라는 값을 새로운 메모리 공간에 저장된 2를 참조하게 됩니다. 그래서 secondNumber를 콘솔에 찍으면 여전히 1이라고 콘솔에 보입니다. number와 secondNumber는 각각 다른 메모리 저장공간을 참조하고 있기 때문이죠.
obj_1를 수정해 봅시다. obj_1.name = ‘park’ 이라고 새로운 값을 할당하면 어떻게 될까요? 객체는 불변성이 없습니다. 그래서 기존 메모리 저장공간에 있는 {name: ‘kim’} 이라는 값이 {name : ‘park’} 으로 바뀌어 버립니다.
원시데이터는 수정을 했을 때 메모리에 저장된 값 자체는 바꿀 수 없고, 새로운 메모리 저장공간에 새로운 값을 저장합니다. 원시데이터가 아닌 데이터는 수정했을 때 기존에 저장되어 있던 메모리 저장공간의 값 자체를 바꿔버립니다.
(2) 리액트에서 불변성이 가지는 의의
리액트에서는 화면을 리렌더링 할지 말지 결정할 때 state의 변화를 확인합니다. state가 변했으면 리렌더링 하는 것이고, state가 변하지 않았으면 리렌더링을 하지 않죠.
그때, state가 변했는지 변하지 않았는지 확인하는 방법이 state의 변화 전, 후의 메모리 주소를 비교합니다. 그래서 만약 리액트에서 원시데이터가 아닌 데이터를 수정할 때 불변성을 지켜주지 않고, 직접 수정을 가하면 값은 바뀌지만 메모리주소는 변함이 없게 되는 것이죠. 그래서 즉, 개발자가 값은 바꿨지만 리액트는 state가 변했다고 인지하지 못하게 됩니다. 그래서 결국 마땅히 일어나야 할 리렌더링이 일어나지 않게 되죠.
(3) 리액트 불변성 지키기 예시
배열을 setState 할 때 불변성을 지켜주기 위해, 직접 수정을 가하지 않고 전개 연산자를 사용해서 기존의 값을 복사하고, 그 이후에 값을 수정하는 식으로 구현합니다.
import React, { useState } from "react";
function App() {
const [dogs, setDogs] = useState(["말티즈"]);
function onClickHandler() {
// spread operator(전개 연산자)를 이용해서 dogs를 복사합니다.
// 그리고 나서 항목을 추가합니다.
setDogs([...dogs, "시고르자브르종"]);
}
console.log(dogs);
return (
<div>
<button onClick={onClickHandler}>버튼</button>
</div>
);
}
export default App;