Hook은 React 버전 16.8부터 React 요소로 새로 추가되었다. Hook을 이용하여 기존 Class 바탕의 코드를 작성할 필요 없이 상태 값과 여러 React의 기능을 사용할 수 있다.
useState는 현재의 state값과 이 값을 업데이트 하는 함수를 쌍으로 제공한다.
쉽게 말하자면 변수를 선언하는 것이라고 생각하면 되겠다.
const [변수명, set변수명] = useState(변수의 초기 값);
예시로 초기값이 0인 test
라는 state를 만들면 다음과 같다.
const [test, setTest] = useState(0);
여기서 set
이 붙은 setTest
는 test
라는 state의 값을 변경할 때 사용하는 함수라고 생각하면 된다.
간단하게 지금 0으로 지정된 test
를 1로 바꾸게 하고 싶다면 아래와 같이 선언해주면 된다.
setTest(1)
이제 아주 간단한 예제를 만들어보자.
버튼을 누르면 1씩 더해지는 함수를 만들어보면 아래와 같이 작성할 수 있다.
import React, { useState } from 'react'
function First() {
const [test, setTest] = useState(0);
const countUp = () => {
setTest(test+1);
};
return (
<div>
{test}
<button onClick={countUp}>카운트업!</button>
</div>
)
}
export default First
여기서 핵심은 사용자에게 표시되는 값이 상호작용에 따라 변경될 수 있도록 하기 위해 useState를 쓴다는 점을 기억하자!
useEffect를 공부 하기 전에 react의 생명주기 개념 부터 알아보자!
react의 component에는 생명 주기(Life Cycle)
이라는 것이 있다.
말 그대로 컴포넌트가 표시되고 사라지는 순간 까지를 하나의 생명 주기로 표현한 것이다.
우리가 사용할 시점은 크게 3가지로 나뉜다.
1. mount(컴포넌트가 표시될 때)
2. update(컴포넌트 내부의 요소가 업데이트 될 때)
3. unmount(컴포넌트가 사라질 때)
우리가 알아볼 useEffect는 이 시점 중에서 하나를 골라, 해당 시점에 특정한 함수가 실행되도록 하는 Hook이다.
useEffect
는 어떤 시점에 함수를 실행시킬지에 따라, 아래와 같이 사용할 수 있다.
useEffect(컴포넌트가 표시될 때 실행할 함수, []);
useEffect(오른쪽 리스트에 적은 요소들이 업데이트되는 시점에 실행할 함수, [업데이트 되는지 지켜볼 변수/State]);
useEffect(() => {return () => 컴포넌트가 사라질 때 실행할 함수})
간단한 로딩 화면을 구현해보자.
isLoaded
가 true면 로딩 화면이 나타나고, false면 나타나지 않게 하는 것을 생각하고 만들어 보자.
우선 첫 번째로 로딩 여부를 위한 state를 만들어야 한다.
const [isLoaded, setIsLoaded] = useState(false)
그리고 이 로딩 완료 여부에 따른 화면이 달라지도록 해준다. '삼항 연산자'를 써서 만들어보면 아래와 같다.
<div>
{isLoaded ? <>로딩완료!</> : <> 로딩 중... </>
</div>
여기서 화면에 진입한 후 3초 뒤에 로딩 여부를 true 바꾸도록 setTimeout()
함수를 이용해준다.
useEffect(() => {
setTimeout(() => {setIsLoaded(true)}, 3000)
}, [])
위에 작성한 것을 완성시키면 다음과 같이 나타낼 수 있다.
import React, { useState, useEffect } from 'react'
function Loading() {
const [isLoaded, setIsLoaded] = useState(false)
useEffect(() => {
setTimeout(() => {setIsLoaded(true)}, 3000)
}, [])
return (
<div>
{isLoaded ? <>로딩완료!</> : <>로딩 중</>}
</div>
)
}
export default Loading
위의 예제에서 로딩이 완료되면 특정한 문자가 나오게 끔 추가를 해보자.
먼저 문자 표시를 위한 state를 선언해준다.
const [text, setText] = useState([])
그 다음 isLoaded가 변하면 text가 추가되도록 만들어 준다.
useEffect(() => {
const temp = text.concat(['추가!'])
setText(temp)
}, [isLoaded])
이를 완성 시키면 로딩이 완료되는 순간에 추가! 라는 텍스트가 추가된다.
import React, { useState, useEffect } from 'react'
function Loading() {
const [isLoaded, setIsLoaded] = useState(false)
const [text, setText] = useState([])
useEffect(() => {
setTimeout(() => {setIsLoaded(true)}, 3000)
}, [])
useEffect(() => {
const temp = text.concat(['추가!'])
setText(temp)
}, [isLoaded])
return (
<div>
{isLoaded ? <>로딩완료!</> : <>로딩 중</>}
{text}
</div>
)
}
export default Loading
추가적으로 디펜던시에 text 를 추가하게 된다면
useEffect(() => {
const temp = text.concat(['추가!'])
setText(temp)
}, [isLoaded, text])
text 가 바뀔때마다 또 실행되기 때문에, 계속 추가되면서 눈에 보이게 된다!
타이머 예제를 만들어 보면서 위의 예시들과 함께 모두 접목해보자.
1초마다 내려가는 타이머 만들기
const [seconds, setSeconds] = useState(60);
cosnt countDown = setTimeout(() => {
is (seconds > 0 ) {
// 초가 0보다 크면 1을 감소
setSeconds(seconds - 1);
} else if (seconds === 0) {
// 0초가 되면 해당 함수 실행 중단
clearTimeout(countDown);
}
}, 1000);
unmount
도 같이 사용해준다.useEffect(() => {
cosnt countDown = setTimeout(() => {
is (seconds > 0 ) {
// 초가 0보다 크면 1을 감소
setSeconds(seconds - 1);
} else if (seconds === 0) {
// 0초가 되면 해당 함수 실행 중단
clearTimeout(countDown);
}
}, 1000);
// 페이지에서 벗어나면, 해당 함수도 이제는 작동하지 않도록 없애준다.
return () => clearTimeout(countDown);
}, [seconds]);
이제 이것을 정리하면 다음과 같이 쓸 수 있다.
// Timer.js
import React, { useState, useEffect } from 'react'
function Timer() {
const [seconds, setSeconds] = useState(60);
useEffect(() => {
const countDown = setTimeout(() => {
if (seconds > 0) {
// 초가 0보다 크면, 1을 감소시킵니다.
setSeconds(seconds - 1);
} else if (seconds === 0) {
// 만약에 초가 0이 되면, 해당 함수가 실행되는 걸 그만해주세요!
clearTimeout(countDown)
}
}, 1000);
// 페이지에서 벗어나면, 해당 함수도 이제는 작동하지 않도록 없애주세요!
return () => clearTimeout(countDown);
}, [seconds]);
return (
<div>
<h1>우리의 타이머</h1>
<h1>{seconds}</h1>
</div>
)
}
export default Timer
클래스형 컴포넌트의 생명 주기는 아래와 같다.
constructor 안에 작성한 것이 render 이전에 실행이 된다!
그리고 componentDidMount 가 컴포넌트/요소가 생성될 때, → 빈대괄호 useEffect
componentDidUpdate 가 컴포넌트/요소가 업데이트될 때, → 대괄호안에 값이 있는 useEffect
componentWillUnmount 가 컴포넌트가 제거될 때 실행되는 부분입니다! → useEffect 안에서 return
useEffect 와 사실상 대응되는 것을 알 수 있다.
특정한 버튼이 눌러졌을때만 해당 값을 받아서 상태값을 변경하도록 하고 싶다면 useRef 를 쓸 수 있다.
getElementById 등과 동일한 역할을 한다고 생각하시면 되며, 아래의 예시처럼 작성하면, 키가 눌러졌을때 해당 값을 받아오게 된다.
import React, { useRef } from 'react'
function Refs() {
const inputRef = useRef()
const keyHandler = () => {
console.log(inputRef.current.value)
}
return (
<>
<input ref={inputRef} onKeyDown={keyHandler}/>
</>
)
}
export default Refs
useRef 는 아래처럼 값이 변해도, 재렌더링 되지 않는 변수를 만들고자 할 때도 사용할 수 있다.
변수의 경우, 컴포넌트가 다시 렌더링될 때 다시 선언이 되게 되는데, useRef 값의 경우, 컴포넌트가 다시 렌더링 되더라도 변한 그 값이 그대로 남아있게 된다는 점에서 차이가 있다!
import React, { useRef } from 'react'
function Refs() {
const inputRef = useRef(0)
const keyHandler = () => {
console.log(inputRef.current + 1)
}
return (
<>
<p>{inputRef.current}</p>
<button onClick={keyHandler}>버튼!</button>
</>
)
}
export default Refs
useReducer 는, 상태 관리를 담당하는 Hook 이다.
조금 더 풀어서 말하자면, useState 를 통한 상태 관리 및 데이터 추가/제거/수정 등의 각종 Handler 를 하나의 함수로 사용할 수 있도록 해준다!
기본적인 형태는 아래와 같습니다.
const [state, dispatch] = useReducer(리듀서 이름, 초기 데이터)
state는 말그대로 상태를 뜻하며, 이 state 안에 각종 데이터가 들어가게 된다.
처음에 초기 데이터를 1 로 설정하면, 이 state 는 1 이 되는 것이다.
dispatch 는 데이터 추가/제거/수정 등을 위한 함수를 뜻한다. dispatch({type:’REMOVE’})
이런식으로 써주면, 알아서 데이터 추가 작업이 진행되는 것이다!
즉, 특정한 작업 타입을 명시해주면, 해당 작업 타입에 따라 일정한 함수를 실행해주는 역할을 담당하게 된다.
dispatch 가 어떤 작업 타입을 입력받았을 때, 어떤 작업을 수행할 지는 아직 정해져 있지 않으므로, 해당 부분은 우리가 직접 정해주어야 한다!
그것을 정의하는 부분이 바로 Reducer
다!
아래처럼 작성해주면, dispatch 함수 실행 시 type: ADD 라는 입력이 들어왔을 때, 그 아래의 동작들을 실행하게 된다.
export function userReducer(state, action) {
switch (action.type) {
case 'ADD':
return [...state, action.data]
case 'REMOVE':
return ~~~
default:
throw new Error(`Unhandled action type: ${action.type}`)
}
}
여기서 state
는 말그대로 현재 상태값, action
은 dispatch 함수 실행 시 인자로 넣어준 그 값을 말한다.
그래서 type: ADD 라고 인자에 명시하면, action.type
을 통해서 그 값을 가져올 수 있게 되는 것이다.
다음과 같은 데이터를 토대로 데이터를 보여주고, 삭제하는 컴포넌트를 만들어보자
export const userData = [
{
id: 1,
name: 'Leanne Graham',
email: 'Sincere@april.biz',
},
{
id: 2,
name: 'Ervin Howell',
email: 'Shanna@melissa.tv',
},
{
id: 3,
name: 'Clementine Bauch',
email: 'Nathan@yesenia.net',
}
]
UserList.jsx
라는 컴포넌트를 하나 만들고, useState 를 써서 아래처럼 작성해보자.
import React, { useState } from 'react'
import { userData } from '../constants/userData'
function UserList() {
const [users, setUsers] = useState(userData)
return (
<div>
{users.map((user) => {
return <p>{user.name}</p>
})}
</div>
)
}
export default UserList
useState 를 쓸 경우, 데이터를 추가하고자 한다면, 추가하는 함수를 따로 만들어 주어야 한다.
그러한 state 관리 로직이 한 컴포넌트에 너무 많아져도 곤란하고, 너무 흩어져 있어도 곤란하다.
그래서 상태 관리를 조금 더 정돈된 상태에서 하도록 도와주는 useReducer 를 사용해보자!
우선 userReducer.js
라는 파일명으로, reducer 를 만들고, ADD작업만 존재한다고 생각해 보자.
export function userReducer(state, action) {
switch (action.type) {
case 'ADD':
return [...state, action.data]
default:
throw new Error(`Unhandled action type: ${action.type}`)
}
}
UserList 컴포넌트를 아래와 같이 작성해준다.
여기서 useState를 사용한 것이 아닌 useReducer를 사용했다는 것이 핵심이다.
import React, { useReducer } from 'react'
import { userReducer } from '../reducers/userReducer'
import { userData } from '../constants/userData'
function UserList() {
const [state, dispatch] = useReducer(userReducer, userData)
return (
<div>
{state.map((user) => {
return <p>{user.name}</p>
})}
</div>
)
}
export default UserList
이제 dispatch 를 통해서 데이터를 추가해보자.
import React, { useReducer, useState } from 'react'
import { userReducer } from '../reducers/userReducer'
import { userData } from '../constants/userData'
function UserList() {
const [state, dispatch] = useReducer(userReducer, userData)
const addUserHandler = () => {
dispatch({
type: 'ADD',
data: { id: 11, name: '11', email: '11' },
})
}
return (
<div>
{state.map((user) => {
return <p>{user.name}</p>
})}
<button onClick={addUserHandler}>dispatch</button>
</div>
)
}
export default UserList
그런데 지금은 고정된 값을 넘겨주도록 되어있다.
사용자로부터 input 을 받아서 추가해보자!
import React, { useReducer, useState } from 'react'
import { userReducer } from '../reducers/userReducer'
import { userData } from '../constants/userData'
function UserList() {
const [userInput, setUserInput] = useState({
id: '',
name: '',
email: '',
})
const userInputHandler = (e) => {
const { name, value } = e.target
setUserInput({ ...userInput, [name]: value })
}
const [state, dispatch] = useReducer(userReducer, userData)
const addUserHandler = (userInput) => {
dispatch({
type: 'ADD',
data: { id: userInput.id, name: userInput.name, email: userInput.email },
})
}
return (
<div>
{state.map((user) => {
return <p>{user.name}</p>
})}
<input name="id" onChange={userInputHandler}></input>
<input name="name" onChange={userInputHandler}></input>
<input name="email" onChange={userInputHandler}></input>
<button onClick={() => addUserHandler(userInput)}>dispatch</button>
</div>
)
}
export default UserList
지금은 useReducer 를 쓰는게 복잡해 보이겠지만, 로직이 많아지면 많아질수록 코드가 훨씬 간단하고 깔끔해보일 것이다.
useContext는 특정 값을 모든 컴포넌트에서 사용할 수 있도록 만들어 놓고, 어디서든 특정한 값을 불러와서 사용할 수 있도록 해주는 Hook 이다.
아래처럼 우선 context 를 하나 만들자.
const UserContext = createContext()
아래처럼 방금 생성한 context 를 통해서 특정한 컴포넌트를 감싸주면, count: 1 이라는 값을 하위 컴포넌트에서 항상 사용할 수 있게 된다!
<UserContext.Provider value={ count: 1 }>
<UserList />
</UserContext.Provider>
하위 컴포넌트에서는 앞서 만들어놓은 context 를 불러온 후에, useContext 를 써주면 Provider 의 value 부분을 통해서 넘겨준 그 값을 바로 받아올 수 있게 된다.
import { UserContext } from '../App'
const { count } = useContext(UserContext)
UserList 안에 UserDetail 안에 UserInfo 가 있는 형태라고 생각해보자.
app.jsx 에서 생성한 데이터를 UserInfo 까지 내려주려면
import UserList from './components/UserList'
function App() {
const [count, setCount] = useState(1)
return (
<UserList count={count}/>
)
}
export default App
import UserDetail from './components/UserDetail'
function UserList({ count }) {
return (
<UserDetail count={count}/>
)
}
export default UserList
import UserInfo from './components/UserDetail'
function UserDetail({ count }) {
return (
<UserInfo count={count}/>
)
}
export default UserDetail
function UserInfo({ count }) {
return (
<p>{count}</p>
)
}
export default UserInfo
→ 바로 이런 현상을 **props drilling**
이라고 한다.
대표적인 anti-pattern 으로, 작동은 하겠지만, 절대로 이렇게 코드를 작성하면 안된다!
이걸 useContext
를 통해서 해결한다면 아래와 같다.
import { createContext, useReducer } from 'react'
import UserList from './components/UserList'
import { userReducer } from './reducers/userReducer'
import { userData } from './constants/userData'
export const UserContext = createContext(null)
function App() {
const [count, setCount] = useState(1)
return (
<UserContext.Provider value={{ count }}>
<UserList />
</UserContext.Provider>
)
}
export default App
import { UserContext } from '../App'
function UserInfo() {
const { count } = useContext(UserContext)
return (
<p>{count}</p>
)
}
export default UserInfo
참고로 항상 useContext 를 써야하는건 아니다.
정말로 전 컴포넌트에서 공유되어야 하는 값이 있을 때만 사용하는 것이고, 각 컴포넌트로 별로만 관리하는 값이라면 그냥 useState 를 쓰는 것이 더 적절하다.