가장 기본적인 hook. 함수형 컴포넌트 내에서 가변적인 상태를 갖게 하는 hook.
const [state, setState] = useState(초기값);
원래 useState가 리턴하는 값은 배열인데, 이걸 구조분해 할당을 통해 받은 것
function App() {
const [number,setNumber] = useState(0);
return (
<>
<div>Number State :{number}</div>
{/* 기존 업데이트는 setNumber(number + 1) */}
{/* 함수형 업데이트를 통한 상태 관리 */}
<button onClick={() => {
setNumber((currentNumber) => {
return currentNumber + 1;
})
}}>State 변경</button>
</>
);
}
컴포넌트가 렌더링 될 때마다 특정 작업을 수행해야 할때 사용
컴포넌트가 화면에 보여졌을때, 화면에서 사라졌을때 설정
useState와 함께 사용하면, state가 변경될때마다 useEffect가 실행되므로 이걸 막기 위해 의존성 배열을 설정해야 한다.
의존성 배열이란, 이 배열에 값을 넣으면 그 값이 바뀔때만 useEffect를 실행한다라는 의미.
function App() {
// useState는 state가 변경 될때마다 컴포넌트를 리렌더링함
// 이때문에 useEffect가 다시 실행됨
// 이걸 해결하기 위해 의존성 배열이 필요
const [value, setValue] = useState('')
// 매개변수로 콜백함수로 들어감
// 컴포넌트가 렌더링 될때 실행됨
useEffect(() => {
console.log('hello useEffect')
// 마지막에 배열형태를 넣어주어서 의존성 배열 설정
// 어떤 값이 바뀔때만 호출되게 하고 싶다.
// 배열 안에 어떠한 값도 넣지 않으면, 어떤 값이 변경되더라도 useEffect를 다시 호출하지 않겠다.
// clean up 기능
// 컴포넌트가 사라질때 동작하는 함수를 만들어서 리턴
return () => {
// 이 부분에 원하는 로직을 작성하면 된다.
}
},[])
return (
<>
<div>
<input
type='text'
value={value}
onChange={(e) => {
setValue(e.target.value)
}}/>
</div>
</>
);
}
export default App;
저장 공간으로서의 useRef와 DOM 요소 접근 방법으로의 useRef가 있다.
저장공간으로서의 사용된다는것은, state와 비슷한 역할을 하지만, state는 변화가 일어나면 렌더링이 다시 일어나서 내부변수가 초기화되만 ref에 저장한 값은 렌더링을 일으키지 않는다. 즉, ref값이 변경되도 렌더링으로 인해 내부 변수들이 초기화 되는 것을 막을 수 있다.
이 값이 변경되도 리렌더링 될 필요가 없는 값에 사용하면 될 것이다.
정리하면 state는 리렌더링이 필요한 값을 다룰때, ref는 리렌더링을 발생시키지 않는 값을 저장할때 사용한다.
// 기존 js에서의 방법
const div = document.getElementById('div')
// useRef를 사용한 방법
export default function InputSample() {
// 여러개의 문자열을 가지고있는 객체형태의 문자열을 관리해주어야 한다
const [inputs, setInputs] = useState({
name:"",
nickname:""
})
// useRef를 담을 객체를 선언한다.
const nameInput = useRef();
const {name, nickname} = inputs;
const onChange = (e) => {
const {name,value} = e.target
const nextInputs = {...inputs, [name] : value}
setInputs(nextInputs)
}
// useRef가 호출될 곳에서 불러주면 된다.
const onReset = () => {
setInputs({
name:'',
nickname:''
})
// 현재 우리가 선택하고 싶은 DOM이 current
nameInput.current.focus();
}
return (
<div>
{/* 현재 초기화 버튼을 누르면 포커스가 초기화 버튼에 남아있다. */}
{/* 포커스를 바꿔주고 싶다면 DOM에 직접 접근해야 한다. */}
{/* 선언한 useRef객체를 접근하고 싶은 DOM에 ref값으로 넣어주고 */}
<input name="name" placeholder='이름' onChange={onChange} value={name} ref={nameInput}/>
<input name="nickname" placeholder='닉네임' onChange={onChange} value={nickname}/>
<button onClick={onReset}>초기화</button>
<div>
<b>값: </b>
{name} - {nickname}
</div>
</div>
)
}
특정 조건을 만족하면 포커스를 이동시키게 하기
function App() {
const [text, setText] = useState("")
const onChange = (e) => {
setText(e.target.value)
console.log(text)
}
const idRef = useRef("")
const pwRef = useRef("")
useEffect(() => {
idRef.current.focus()
},[])
// id의 state가 10자리가 넘어가면 자동으로 pw에 포커스를 가게
useEffect(() => {
if(text.length >=10){
pwRef.current.focus();
}
},[text])
return (
<>
<div>
아이디: <input onChange={onChange} type="text" ref={idRef}/>
</div>
<div>
비밀번호: <input type="passwrod" ref={pwRef}/>
</div>
</>
);
}
export default App;
function App() {
// 이 값이 바뀐다고 해서 컴포넌트가 리렌더링 될 필요가 없으니 useRef를 이용해서 관리
const nextId = useRef(4);
// users객체가 앞으로 새로 늘어난다 하더라도, id값이 바뀐다 하더라도 렌더링 될 필요가 없다
const onCreate = () =>{
console.log(nextId.current)
nextId.current += 1;
}
const users =[
{
id:1,
username:'carllis',
email:'oterte@naver.com'
},
{
id:2,
username:'pocket',
email:'oterte2891@gmail.com'
},
{
id:3,
username:'Liz',
email:'liz@example.com'
}
]
return (
<UserList users={users}/>
);
}
export default App;
// state와 ref의 차이
function App() {
const [count, setCount] = useState(0)
const countRef = useRef(0)
// 이 값은 바뀔때마다 렌더링이 일어남
const stateHandler = () => {
setCount(count + 1)
}
// 이 값은 바뀌어도 렌더링이 안일어남
const refHandler = () => {
countRef.current++;
}
return (
<>
<div>
state 영역 {count}
<button onClick={stateHandler}>state 증가</button>
</div>
<div>
ref 영역 {countRef.current}
<button onClick={refHandler}>ref 증가</button>
</div>
</>
);
}
export default App;
우리는 일반적으로 부모 컴포넌트에서 자식 컴포넌트로 데이터를 넘겨줄때 props를 사용하여 전달한다
그러나 부모-자식->그 자식-> 그 자식의 자식 .... 쭉 이어지다보면 prop drilling 현상이 일어난다.
prop drilling의 문제점은 1) 깊이가 너무 깊어지면 이 prop이 어떤 컴포넌트로부터 왔는지 파악하기가 어렵다, 2) 어떤 컴포넌트에서 오류가 발생할 경우 추적이 힘들어져 대처가 늦어진다.
이 문제를 해결하려고 나온것이 context api.
useContext hook을 통해 전역 데이터를 쉽게 관리할 수 있게 됨.
1. context api 필수개념
// FamilyContext.js
export const FamilyContext= createContext(null);
// GrandFather.js
export default function GrandFather() {
// 사용할 props
const houseName = "스파르타"
const money = 10000;
return (
// 더이상 props는 필요없다 왜냐?
// 우리는 props로 값을 내려주는게 아니라 context로 값을 내려주기 때문
// 우리가 작성한 context 컴포넌트 파일에 .Provider를 붙이고, value를 객체형태로 설정
<FamilyContext.Provider value={{
houseName:houseName,
money:money
}}>
<Father />
</FamilyContext.Provider>
)
}
//Father.js
export default function Father() {
return (
<div>
<Child />
</div>
)
}
//Child.js
export default function Child() {
// context로부터 데이터 받아오기
// 이 data 안에는 우리가 value값으로 잡아줬던 값들이 남아잇다.
const data = useContext(FamilyContext)
return (
<div>
우리집 이름은 {data.houseName}이고 용돈은 {data.money}원이다
</div>
)
}
// App.js
function App() {
return (
<>
<GrandFather />
</>
);
}
export default App;
// 현재 렌더링이 필요한 구간은 App.js지만,
// 불필요하게 하위 컴포넌트인 Box1,2,3도 값이 바뀔 때마다 같이 렌더링 되고 있다.
// 이러한 현상을 React.memo를 통해 해결
function App() {
const [count, setCount] = useState(0)
const onIncrease = () => {
setCount((prev) => prev + 1)
}
const onDecrease = () => {
setCount((prev) => prev - 1)
}
return (
<>
<h3>count : {count}</h3>
<button onClick={onIncrease}>+</button>
<button onClick={onDecrease}>-</button>
<div style={{
display: "flex",
}}>
<Box1 count={count} />
<Box2 count={count} />
<Box3 count={count} />
</div>
</>
);
}
// React.memo를 이용해 최적화 하기
// Box1.js
function Box1() {
console.log('박스1 렌더링')
return (
<div style={{
width:'100px',
height:'100px',
backgroundColor:'green'
}}>
<h1>
box1
</h1>
</div>
)
}
// 외부에 내보낼 때 이렇게 작성하면 된다.
export default React.memo(Box1);
useCallback이란 인자로 들어오는 함수 자체를 기억하는 것
// Box1.js
// 분명 메모이제이션을 했지만, 카운트의 값이 바뀔때마다 이 컴포넌트도 리렌더링 된다.
// 그 이유는 함수형 컴포넌트를 사용했기 때문.
// 이를 해결하기 위하여 useCallback을 사용하여 함수를
function Box1({onReset}) {
console.log('박스1 렌더링')
return (
<div style={{
width:'100px',
height:'100px',
backgroundColor:'green'
}}>
<button onClick={onReset} >초기화</button>
</div>
)
}
export default React.memo(Box1)
// App.js
function App() {
const [count, setCount] = useState(0)
const onIncrease = () => {
setCount((prev) => prev + 1)
}
const onDecrease = () => {
setCount((prev) => prev - 1)
}
// 카운트 초기화 함수
// 이 함수는 바뀐것이 없다.
// 근데 왜 box1은 props로 받아온 값이 바뀌었다고 생각할까?
// 컴포넌트가 리렌더링 되면 이 함수도 새로운 주소값을 가지게 된다.
// 그래서 box1은 props가 바뀌었다고 생각하고 다시 렌더링 되는 것
// 이걸 위해서 useCallback을 통해서 함수를 메모이제이션
// 별도 메모리 공간에 저장하고 특정 조건이 아니면 바뀌지 않겠다.
// const onReset = () => {
// setCount(0)
// }
// useCallback은 컴포넌트가 처음 그려질때의 모습을 저장하기 때문에,
// count를 올려서 콘솔에 찍어도 0이 나온다
// 이걸 방지하려면 count가 변경될때 만큼은 콜백도 변경되어야 한다
// 그럴때 의존성 배열에 그 값을 넣어준다
const onReset = useCallback(() => {
setCount(0)
},[count])
return (
<>
<h3>count : {count}</h3>
<button onClick={onIncrease}>+</button>
<button onClick={onDecrease}>-</button>
<div style={{
display: "flex",
}}>
<Box1 onReset={onReset}/>
<Box2 />
<Box3 />
</div>
</>
);
}
export default App;
// 엄청 무거운 로직이 있다고 가정
export default function HeavyComponents() {
const [count, setCount] = useState(0);
// 이 작업으로 state가 변경되면서 컴포넌트가 리렌더링되고
const onIncrease = () => {
setCount((prev) => prev + 1 );
}
// 이 함수는 컴포넌트가 리렌더링 될 때마다 다시 작동함
// 이 무거운 작업이 리렌더링 될때마다 수행된다는건 너무나 비효율적
const heavyWork = () => {
for(let i = 0; i<10000000; i++){}
return 100;
}
// 이 부분은 컴포넌트가 리렌더링이 되면 항상 호출되는 부분
// 이 값이 useMemo를 사용하여 메모리에 저장하면 좋지않을까?
// const value = heavyWork();
const value = useMemo(() => heavyWork() , [])
console.log(`value는 ${value}다`)
return (
<>
<p>나는 엄청 무거운 컴포넌트</p>
<button onClick={onIncrease}>누르면 아래 카운트가 올라간다.</button><br/>
<h1>{count}</h1>
</>
)
}