✨ state는 리액트에서 가장 기초가 되면서,
✨ 렌더링에 가장 큰 영향을 끼치는 요소입니다.
State
란 컴포넌트 내부에서 바뀔 수 있는 값을 의미한다.
이뉴는 바로 UI 엘리먼트
의 반영을 위해서이다.
우리가 앞에서 name
이라는 정보를 const name = "민준"
라고 했다면,
만약 name
이라는 값이 바뀌어야만 하는 정보였어야 한다면 state
로 생성합니다.
state를 생성할 때는 useState() 를 사용합니다.
아래 코드를 보면
GrandFather
커포넌트에서 기존에 있었던 const name = "김할아"
라는 코드가
const [name,setName] = useState("김할아");
라는 코드로 사용되고 있다.
import React, { useState } from 'react';
function GrandFather() {
const [name, setName] = useState("김할아"); // 이것이 state!
return <Mother grandFatherName={name} />;
}
// .. 중략
앞으로 useState
라는 함수를 이용해 state
를 만들게 됩니다.
useState
는 state
를 만들어주는 리액트에서 제공하는 기능입니다.
그래서 리엑트에만 존재하는 개념이자 기능입니다.
앞으로 우리는 이것을 기능이 아닌 훅이라고 표현합니다.
const [ value, setValue ] = useState( 초기값 ); // 배열의 구조분해 할당이 사용된걸 확인할 수 있다.
먼저 const
로 선언을 하고 [] 빈 배열
을 생성한 다음,
배열의 첫번째 자리엔 이 state
의 이름, 그리고 두번째 자리에는 set
을 붙여서 state
의 이름을 붙입니다.
그리고 useState()
의 인자에는 이 state
의 원하는 처음 값을 넣어줍니다.
const [name, setName] = useState("김할아");
위에서는 name
이라는 state를 만들었고, name state
의 초기값은 "김할아" 로 지정했습니다.
이때 초기값을 initial state
라고 부릅니다.
state
의 정의처럼 언제든지 변할 수 있는 값이기 때문에 초기값이라는 개념이 존재하는 것입니다.
state를 변경할때는 setValue(바꾸고 싶은 값) 를 사용한다.
state
란 컴포넌트안에서 변할 수 있는 값입니다.
예를 들어서 김할아라는 이름이 백할아로 바뀌었다고 한다면
우리는 setName
을 통해 이름을 바꿀 수 있습니다.
setName("백할아")
로 사용을 하면 이름이 바뀌게 될 것입니다.
// 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;
그러나 위처럼 바뀐 값은 브라우저를 샐로고침하면 다시 초기값으로 바뀝니다.
바꾼 값은 어디에 저장되는 것은 아니기 때문에 단순히 화면에서만
바뀐 값으로 리렌더링 되는 것입니다.
✨ state와 각종 DOM handler event와의 조합을 알아보자
우리는 버튼을 눌렀을 때 하고 싶은 행동을 함수로 제작할건데,
onClickHandler
라는 함수를 만들고 onClick={}
에 넣어주었습니다.
React
에는 이러한 방식으로 함수와 컴포넌트를 연결시킵니다.
우리는 이 함수를 이벤트 핸들러라고 표현합니다.
import React from "react";
function App() {
// 버튼을 눌렀을 때 하고 싶은 행동
function onClickHandler() {
console.log("hello button");
}
return (
<div>
<button onClick={onClickHandler}>버튼</button>
</div>
);
}
export default App;
아래는 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;
버튼을 누르면 setName()
안의 값이 누렁이기 때문에,
최초 state
값 길동이에서 누렁이로 바뀌게 됩니다.
input과 state 구현하기
input에서는 보통 사용자가 입력한 값을 state로 관리하는 패턴을 자주 사용합니다.
import React, { useState } from "react";
const App = () => {
const [value, setValue] = useState("");
return (
<div>
<input type="text" />
</div>
);
};
export default App;
이와 같이 input
과 useState
를 사용해 input
의 값을 넣을 value
라는 state
를 생성합니다.
App
을 보면 function keyword
를 사용하지 않고 화살표 함수를 사용해보았다.
화살표 함수, function 키워드 모두 똑같이 함수 컴포넌트를 만들 수 있습니다.
이벤트 핸들러 구현하고 state
와 연결하기
먼저 input
에 onChange
라는 이벤트를 불러내,
생성한 이벤트 핸들러 함수를 넣습니다.
import React, { useState } from "react";
const App = () => {
const [value, setValue] = useState("");
const onChangeHandler = (event) => {
const inputValue = event.target.value;
setValue(inputValue);
console.log(inputValue);
};
console.log(value) // value가 어떻게 변하는지 한번 콘솔로 볼까요?
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
연결이 끝났습니다.
꼭 onChange
와 value
를 둘 다 연결해야만 한다면
React
에서는 input
요소의 value
속성과 onChange
이벤트를 함께 사용하여
입력 필드의 상태를 관리하는 것이 일반적입니다.
이러한 패턴을 제어 컴포넌트라고 합니다.
React
에 의해 값이 제어되는 컴포넌트가 제어 컴포넌트
= state
에 의해 값이 제어되는 input
컴포넌트는 제어 컴포넌트
제어 컴포넌트에서는 React
의 상태가 소스 오브 트루스 역할을 하며,
이를 통해 입력 필드의 현재 값이 항상 React
컴포넌트의 상태와 동기화됩니다.
source of truth
란 어떤 정보의 정확하고 최신의 상태를 유지하는 주된 위치 또는
저장소를 가리킵니다.
위의 예시에서 리엑트의 state
이 input
이 value
와 엮여있으므로 state
자체가 input
의
변경된 정보 자체를 가리키게 됩니다.
데이터가 일관성 있고, 정확성을 유지하기 위해 필수입니다.
이와 반대되게 value
를 연결하지 않는 비제어 컴포넌트가 있는데,
useState
를 사용하면서 input
요소의 value
속성에 상태를 연결하지 않는 경우,
이는 비제어 컴포넌트와 유사한 동작을 하게 됩니다.
아래는 비제어 컴포넌트의 예시 코드이다.
import React, { useState } from "react";
const App = () => {
const [inputValue, setInputValue] = useState("");
const onChangeHandler = (event) => {
setInputValue(event.target.value);
};
return (
<div>
<input type="text" onChange={onChangeHandler} />
<p>입력값: {inputValue}</p> {/* 입력값을 화면에 표시 */}
</div>
);
};
export default App;
주의할 점은 상태 업데이트 지연과 비제어 컴포넌트이다.
위의 방식은 특정 상황에서 유용할 수도 있지만,
일반적으로 폼 데이터를 더 명확하게 제어하고 싶다면 제어 컴포넌트 방식이 좋다.
✨ 불변성은 리액트의 state 개념을 이해하기 위해 필요한 개념이다.
불변성이란?
불변성이란 메모리에 있는 값을 변경할 수 없는 것을 의미합니다.
자바스크립트 데이터 형태중에 원시 데이터는 불변성
이 있고,
원시 데이터
가 아닌 객체 배열 함수는 불변성
이 없습니다.
// 불변성 깨뜨리는 방법
let numbers = [1, 2, 3];
numbers.push(4); // 배열에 직접 요소를 추가
console.log(numbers); // [1, 2, 3, 4]
// 불변성 유지하는 방법
let numbers = [1, 2, 3];
let newNumbers = [...numbers, 4]; // 새 배열을 생성하여 기존 배열을 변경하지 않음
console.log(numbers); // [1, 2, 3]
console.log(newNumbers); // [1, 2, 3, 4]
우리가 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 === obj_2
는 false가 됩니다.
원시데이터
로 돌아와서 만약 기존에 1이던 number
를 number = 2
라고 새로운 값을 할당하면
메모리에서는 어떻게 될까요? 원시 데이터
는 불변성
이 있기 때문에
기존 메모리에 저장이 되어 있는 1이라는 값이 변하지 않고, 새로운 메모리 저장공간
에 2가 생기고,
number
라는 값을 새로운 메모리 공간에 저장된 2를 참조하게 됩니다.
그래서 secondNumber
를 콘솔에 찍으면 여전히 1이라고 콘솔에 보인다.
number
와 secondNumber
는 각각 다른 메모리 저장공간을 참조하고 있기 때문입니다.
obj_1
를 수정해보면 obj_1.name = 'park'
라고 새로운 값을 할당하면
객체는 불변성이 없기 때문에 기존 메모리 저장공간에 있는 {name:'kim'}
이란 값이
{name:'park'}
으로 바뀌어 버립니다.
원시 데이터
는 수정을 했을 때 메모리에 저장된 값 자체는 바꿀 수 없고,
새로운 메모리 저장공간에 새로운 값을 저장합니다.
원시데이터
가 아닌 데이터는 수정했을 때 기존에 저장되어 있던
메모리 저장공간의 값 자체를 바꿔버립니다.
리액트에서는 화면을 리랜더링 할지 말지 결정할 때 state
의 변화를 확인합니다.
state
가 변했으면 리렌더링 하는 것이고, state
가 변하지 않았으면 리렌더링 하지 않습니다.
그때, state
가 변했는지 변하지 않았는지 확인하는 방법이 state
의 변화 전,
후의 메모리 주소를 비교합니다.
만약 리액트에서 원시데이터
가 아닌 데이터를 수정할 때 불변성을 지켜주지 않고,
직접 수정을 가하면 값은 바뀌지만 메모리주소는 변함이 없게 되는 것입니다.
그래서 개발자가 값은 바꿨지만 리액트는 state
가 변했다고 인지하지 못하게 됩니다.
결국 마땅히 일어나야 할 리렌더링이 일어나지 않게 되는 것이 됩니다.
배열을 setState
할 때 불변성
을 지켜주기 위해 spread operator
를 사용해서
기존의 값을 복사하고, 그 이후의 값을 수정하는 방식을 사용합니다.
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;