#2. props, useState, useEffect, useRef
컴포넌트의 State와 Props
State는 해당 컴포넌트가 관리하는 데이터이고, Props는 부모 컴포넌트로부터 전달받은 데이터입니다. React에서는 UI 데이터는 반드시 Props와 State로 관리하여야 합니다. UI 데이터를 State와 Props로 관리하지 않으면 UI 데이터가 변경되어도 화면이 자동으로 렌더링되지 않을 수 있기 때문입니다.
useState 훅
import React from "react";
let color = "red";
export default function MyComponent() {
function onClick() {
color = "blue";
}
let style = {
backgroundColor: color,
}
return (
<div>
<button style={style} onClick={onClick}>
클릭
</button>
</div>
)
}
위 코드를 실행해보면 초기 화면은 의도대로 빨간색으로 나옵니다. 하지만 버튼을 클릭했을 때 화면은 여전히 바뀌지 않습니다. 이는 실제 데이터는 파란색으로 변경되었지만, 리액트가 UI 데이터가 변경되었다는 사실을 모르기 때문입니다.
따라서 리액트에게 UI 데이터가 변경되었다는 것을 알리려면 상탯값이나 속성값을 이용하여야 합니다.
import React, { useState } from "react";
export default function MyComponent() {
const [color, setColor] = useState("red");
function onClick() {
let myColor = color === "red" ? "blue" : "red";
setColor(myColor);
}
let style = {
backgroundColor: color,
}
return (
<div>
<button style={style} onClick={onClick}>
클릭
</button>
</div >
)
}
컴포넌트에 상탯값을 추가할 때는 useState 훅을 사용합니다. React에서는 setColor() 함수가 호출되면 상탯값을 변경하고 해당 컴포넌트를 다시 렌더링합니다. 따라서 UI 데이터가 변경되는 즉시 화면에 반영할 수 있습니다.
useState 훅 하나로 여러 상탯값 관리하기
import React, { useState } from "react";
export default function MyComponent() {
const [state, setState] = useState({ name: "", age: 0 });
return (
<div>
<p>{`Hi~ My name is ${state.name}`}</p>
<p>{`I'm ${state.age} years old`}</p>
<input
type="text"
value={state.name}
onChange={e => setState({ ...state, name: e.target.value })}
/>
<input
type="number"
value={state.age}
onChange={e => setState({ ...state, age: e.target.value })}
/>
</div>
)
}
두 상탯값을 하나의 객체로 관리합니다. useState 훅은 이전 상탯값을 덮어쓰기 떄문에 ...state와 같은 코드가 필요합니다. 하지만 이렇게 상탯값을 하나의 객체로 관리할 때는 useReducer 훅을 사용하는 것이 더 좋습니다.
props
import React, { useState } from "react";
export default function MyComponent() {
const [count, setCount] = useState(0);
function onClick() {
setCount(prev => prev + 1);
}
return (
<div>
<Title title={`현재 카운트: ${count}`} />
<button onClick={onClick}>증가</button>
</div>
)
}
const Title = React.memo(({ title }) => {
return <h3>{title}</h3>
})
다음은 부모 컴포넌트에서 속성값을 내려받는 예제입니다.
버튼을 클릭하면 count 상탯값이 변경되고 MyComponent 컴포넌트는 다시 렌더링됩니다. 이때 자식 컴포넌트인 Title 컴포넌트는 새로운 title 속성값을 내려받게 됩니다.
이렇게 상탯값과 속성값으로 UI 데이터를 관리하는 것이 React의 핵심이라고 볼 수 있습니다.
Title 컴포넌트는 부모 컴포넌트가 렌더링 될때마다 자동으로 같이 렌더링됩니다. 만약 title 속성값이 변경될 때만 렌더링되길 원한다면 위와 같이 React.memo를 이용하면 됩니다.
컴포넌트마다 고유의 상탯값을 가집니다.
import React from "react";
import MyComponent from "./MyComponent";
export default function App() {
<MyComponent />
<MyComponent />
}
만약 위에서 만든 MyComponent를 상위 컴포넌트인 App 컴포넌트에서 두번 사용하면 어떻게 될까요?
상탯값은 컴포넌트마다 고유의 영역을 가지고 있습니다. 따라서 두 값은 연동되지 않고 각자의 값을 가집니다.
하위 컴포넌트에서 Props값 변경하기
import React, { useState } from "react";
export default function MyComponent() {
const [count, setCount] = useState(0);
return (
<div>
<Counter count={count} setCount={setCount} />
</div>
)
}
function Counter({ count, setCount }) {
return (
<div>
<h3>현재 카운트: {count}</h3>
<button onClick={() => setCount(prev => prev + 1)}>증가</button>
</div>
)
}
기본적으로 props로 내려받은 값은 불변 변수입니다. 따라서 자식 컴포넌트에서 이를 수정하려고 하면 에러가 발생합니다. 하지만 상위 컴포넌트에서 useState로 정의한 상탯값 변경 함수를 props로 하위 컴포넌트에 전달하면 하위 컴포넌트에서도 상위 컴포넌트의 state값을 변경할 수 있습니다.
위 예제를 실행해보면 자식 컴포넌트인 Counter에서 부모 컴포넌트의 상탯값 count를 변경하고 있음을 알 수 있습니다.
useEffect
useEffect 훅은 컴포넌트의 부수 효과를 처리할 때 사용하는 훅입니다. 여기서 부수 효과란 함수 외부의 상태를 변경하는 연산을 뜻합니다. 예를 들면 API를 호출하거나 이벤트 처리 함수를 등록하고 해제하는 것 등이 있습니다.
import React, { useState, useEffect } from "react";
export default function MyComponent() {
const [width, setWidth] = useState(window.innerWidth);
useEffect(() => {
const onResize = () => setWidth(window.innerWidth);
window.addEventListener("resize", onResize);
return () => {
window.removeEventListener("resize", onResize);
};
}, []);
return <div>{`width is ${width}`}</div>
}
useEffect 훅에 입력하는 함수를 부수효과 함수라고 합니다. 부수 효과 함수는 렌더링 결과가 실제 돔에 반영된 후 호출되고(componentDidMount), 컴포넌트가 사라지기 직전(componentWillUnmount)에 마지막으로 호출됩니다.
부수 효과 함수는 함수를 반환할 수 있습니다. 반환된 함수는 부수 효과 함수가 호출되기 직전에 호출되고, 컴포넌트가 사라지기 직전에 마지막으로 호출됩니다. 따라서 이벤트를 해제하는 일에 적절합니다.
useRef
예를 들어 돔 요소에 포커스를 주거나 돔 요소의 크기나 스크롤 위치를 알고 싶은 경우엔 DOM 요소에 직접 접근해야 합니다. 이 때 ref 속성값을 이용하면 자식 요소에 직접 접근할 수 있습니다. 자식 요소는 컴포넌트일 수도 있고 DOM 요소일 수도 있습니다.
import React, { useState, useEffect, useRef } from "react";
export default function MyComponent() {
const inputRef = useRef();
useEffect(() => {
inputRef.current.focus();
}, []);
return (
<div>
<input type="text" ref={inputRef} />
</div>
)
}
useRef 훅이 반환하는 ref 객체를 ref 속성값에 입력합니다. ref 객체의 current 속성을 이용하면 자식 요소에 접근할 수 있습니다. 이 외에도 ref 속성값은 다양한 기능을 가지고 있습니다.
forwardRef
import React, { useState, useEffect, useRef } from "react";
export default function MyComponent() {
const inputRef = useRef();
return (
<div>
<TextInput ref={inputRef} />
<button onClick={() => inputRef.current.focus()}>텍스트로 이동</button>
</div>
)
}
const TextInput = React.forwardRef((props, ref) => (
<div>
<input type="text" ref={ref} />
<button>저장</button>
</div>
))
forwardRef 함수를 이용하면 부모 컴포넌트에서 넘어온 ref 속성값을 직접 처리할 수 있습니다. 이전 코드에서 inputRef로 사용했던 이름을 리액트의 예약어인 ref로 사용할 수 있게 된 것입니다.
ref 속성값으로 함수 사용하기
지금까지는 useRef 훅으로 만들어진 ref 객체를 ref 속성값에 연결하는 경우였습니다. ref 속성값에는 함수를 입력할 수도 있습니다. 함수를 입력하면 자식 요소가 생성되거나 제거되는 시점에 호출됩니다.
import React, { useState, useMemo } from "react";
export default function MyComponent() {
const [text, setText] = useState(INITIAL_TEXT);
const [showText, setShowText] = useState(true);
const setInitialText = useMemo(ref => ref && setText(INITIAL_TEXT));
return (
<div>
{showText && (
<input
type="text"
ref={setInitialText}
value={text}
onChange={e => setText(e.target.value)}
/>
)}
<button onClick={() => setShowText(!showText)}>
{showText ? "가리기" : "보이기"}
</button>
</div>
)
}
const INITIAL_TEXT = "안녕하세요";
useMemo 훅을 이용해서 setInitialText 함수를 변하지 않게 만들었습니다. useMemo 훅의 메모이제이션 기능 덕분에 한번 생성된 함수를 계속 재사용하는 것입니다. 따라서 ref 속성값에 새로운 함수를 입력하지 않으므로 input 요소가 생성되거나 제거될 때만 setInitialText 함수가 호출됩니다.
만일 useMemo 훅을 사용하지 않고 ref 속성값에 직접 setInitialText 함수를 넣게 되면, INITIAL_STATE의 값에서 바뀌지 않습니다. 이는 input 요소에 타이핑을 칠때마다 새로운 함수가 만들어지게 되고 ref 속성값에 연결된 함수가 계속해서 실행되기 때문입니다. 따라서 useMemo 훅과 같은 메모이제이션 기능을 사용해야 합니다.
렌더링과 무관한 값 저장하기: useRef
useRef 훅은 자식 요소에 접근하는 것 외에도 중요한 용도가 한가지 더 있습니다. 컴포넌트 내부에서 생성되는 값 중에는 렌더링과 무관한 값도 있는데, 이 값을 저장할 때 useRef 훅을 사용합니다. 예를 들어 setTimeout이 반환하는 timerID를 저장하거나 이전 상탯값을 저장하는 데에 쓰일 수 있습니다.
import React, { useState, useEffect, useRef } from "react";
export default function MyComponent() {
const [age, setAge] = useState(20);
const prevAgeRef = useRef(20);
useEffect(() => {
prevAgeRef.current = age;
}, [age]);
const prevAge = prevAgeRef.current;
const text = age === prevAge ? "same" : age > prevAge ? "older" : "younger";
return (
<React.Fragment>
<p>{`age ${age} is ${text} than age ${prevAge}`}</p>
<button onClick={() => {
const age = Math.floor(Math.random() * 50 + 1);
setAge(age);
}}>
나이 변경
</button>
</React.Fragment>
)
}