React Hook

변승훈·2022년 8월 5일
0

React의 Hook

Hook은 React 버전 16.8부터 React 요소로 새로 추가되었다. Hook을 이용하여 기존 Class 바탕의 코드를 작성할 필요 없이 상태 값과 여러 React의 기능을 사용할 수 있다.

1. useState

useState란?

useState는 현재의 state값과 이 값을 업데이트 하는 함수를 쌍으로 제공한다.
쉽게 말하자면 변수를 선언하는 것이라고 생각하면 되겠다.

const [변수명, set변수명] = useState(변수의 초기 값);

예시로 초기값이 0인 test라는 state를 만들면 다음과 같다.

const [test, setTest] = useState(0);

여기서 set이 붙은 setTesttest라는 state의 값을 변경할 때 사용하는 함수라고 생각하면 된다.

간단하게 지금 0으로 지정된 test를 1로 바꾸게 하고 싶다면 아래와 같이 선언해주면 된다.

setTest(1)

useState 예제

이제 아주 간단한 예제를 만들어보자.
버튼을 누르면 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를 쓴다는 점을 기억하자!

2. useEffect

useEffect를 공부 하기 전에 react의 생명주기 개념 부터 알아보자!

React의 생명주기

react의 component에는 생명 주기(Life Cycle)이라는 것이 있다.
말 그대로 컴포넌트가 표시되고 사라지는 순간 까지를 하나의 생명 주기로 표현한 것이다.

useEffect란?

우리가 사용할 시점은 크게 3가지로 나뉜다.
1. mount(컴포넌트가 표시될 때)
2. update(컴포넌트 내부의 요소가 업데이트 될 때)
3. unmount(컴포넌트가 사라질 때)

우리가 알아볼 useEffect는 이 시점 중에서 하나를 골라, 해당 시점에 특정한 함수가 실행되도록 하는 Hook이다.

useEffect 는 어떤 시점에 함수를 실행시킬지에 따라, 아래와 같이 사용할 수 있다.

  1. mount (컴포넌트가 표시될 때)
useEffect(컴포넌트가 표시될 때 실행할 함수, []);
  1. update (컴포넌트 내부 요소가 업데이트될 때)
useEffect(오른쪽 리스트에 적은 요소들이 업데이트되는 시점에 실행할 함수, [업데이트 되는지 지켜볼 변수/State]);
  1. unmount (컴포넌트가 사라질 때)
useEffect(() => {return () => 컴포넌트가 사라질 때 실행할 함수})

useEffect 예제

1. mount (컴포넌트가 표시될 때)

간단한 로딩 화면을 구현해보자.

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

2. update (컴포넌트 내부 요소가 업데이트될 때)

위의 예제에서 로딩이 완료되면 특정한 문자가 나오게 끔 추가를 해보자.

먼저 문자 표시를 위한 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 가 바뀔때마다 또 실행되기 때문에, 계속 추가되면서 눈에 보이게 된다!

3. unmount (컴포넌트가 사라질 때)

타이머 예제를 만들어 보면서 위의 예시들과 함께 모두 접목해보자.

1초마다 내려가는 타이머 만들기

  1. useState를 이용해 숫자를 만든다.
const [seconds, setSeconds] = useState(60);
  1. 1초 후 값이 내려가는 함수를 만든다.
cosnt countDown = setTimeout(() => {
	is (seconds > 0 ) {
      // 초가 0보다 크면 1을 감소
      setSeconds(seconds - 1);
    } else if (seconds === 0) {
      // 0초가 되면 해당 함수 실행 중단
    	clearTimeout(countDown);
    }
}, 1000);
  1. seconds 변수가 바뀔 때 마다 (값이 1씩 감소할 대 마다) 1초 후에 또 1이 감소하도록 만들어 준다, 이때 useEffect의 세번 째인 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

life cycle 추가 내용

클래스형 컴포넌트의 생명 주기는 아래와 같다.

constructor 안에 작성한 것이 render 이전에 실행이 된다!

그리고 componentDidMount 가 컴포넌트/요소가 생성될 때, → 빈대괄호 useEffect

componentDidUpdate 가 컴포넌트/요소가 업데이트될 때, → 대괄호안에 값이 있는 useEffect

componentWillUnmount 가 컴포넌트가 제거될 때 실행되는 부분입니다! → useEffect 안에서 return

useEffect 와 사실상 대응되는 것을 알 수 있다.

3. useRef

요소 가져오기

특정한 버튼이 눌러졌을때만 해당 값을 받아서 상태값을 변경하도록 하고 싶다면 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

4. useReducer

useReducer 개념

useReducer 는, 상태 관리를 담당하는 Hook 이다.

조금 더 풀어서 말하자면, useState 를 통한 상태 관리 및 데이터 추가/제거/수정 등의 각종 Handler 를 하나의 함수로 사용할 수 있도록 해준다!

기본적인 형태는 아래와 같습니다.

const [state, dispatch] = useReducer(리듀서 이름, 초기 데이터)
  1. state는 말그대로 상태를 뜻하며, 이 state 안에 각종 데이터가 들어가게 된다.
    처음에 초기 데이터를 1 로 설정하면, 이 state 는 1 이 되는 것이다.

  2. 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 를 쓰는게 복잡해 보이겠지만, 로직이 많아지면 많아질수록 코드가 훨씬 간단하고 깔끔해보일 것이다.

5. useContext

useContext 개념

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 를 쓰는 것이 더 적절하다.

profile
잘 할 수 있는 개발자가 되기 위하여

0개의 댓글