React | Custom Hooks

설탕·2022년 2월 16일
1
post-thumbnail
post-custom-banner

관심사의 분리 (SoC: Separation of Concerns)

하나의 함수, 변수, 클래스, 컴포넌트에 한번에 너무 많은 로직이 들어가 있으면 코드를 파악하기 어렵고 문제의 원인을 찾기 쉽지 않다.

그래서 우리는 코드가 단위별로 하나의 관심사만 갖도록 하고 그 관심사에 대해서만 충실히 동작하도록 만들어야 한다. 컴퓨터 공학에서는 이렇게 각각의 관심사에 따라 코드를 분리하는 기법을 관심사의 분리라고 부른다.

관심사의 분리를 하게 되면 어떤 문제가 생겼을 때 전체 기능을 파악하기 위해 읽어야 하는 코드의 단위가 줄어들게 되고, 코드에 대한 파악이 빨라지므로 문제를 효과적으로 해결할 수 있게 된다. 즉 Divide & Conquer(분할 - 정복)이 보다 쉬워지는 것이다.
또한 하나의 수정사항으로 인해 전체가 수정되는 것이 아니라, 해당 사항이 있는 일부분만 변경시키기 때문에 변화에 대해서도 내구성을 갖추게 된다.

관심사의 분리가 적절히 구현된 코드에서는 다음과 같은 특성을 발견할 수 있다.

  • Loose Coupling: 낮은 결합도, 각각의 코드가 서로 얽혀있지 않고 독립적으로 잘 분리되어 있음
  • High Cohesive: 높은 응집도, 유사한 내용끼리 비슷한 위치에 잘 모여 있음

관심사의 분리가 가지는 장점

  • 코드가 더욱 명료해진다: 자신이 어떤 일을 하고, 어떤 목적을 가지고 설계된 코드인지 보다 잘 드러나게 된다.
  • 코드 재사용성이 높아진다: 여러 역할이 엉켜있는 코드보다, 역할 별로 잘 분리되어 있는 코드를 재사용하기가 쉽다.
  • 유지·보수가 용이하다: 변경 사항이 발생했을 때 해당 관심사에 연관된 코드만 수정하면 된다.
  • 테스트 코드를 작성하기 쉽다: 얽혀있는 로직보다 분리되어 있는 로직에 대한 테스트가 보다 더 간단하다.

Custom Hooks

View와 Logic의 분리

다음 코드는 하나의 컴포넌트 안에 View와 Logic이 혼재하고 있다.

const UserList = ({ users }) => {
  const [users, setUsers] = useState([])

  useEffect(() => {
    fetchUsers("/users")
      .then(res => res.json())
      .then(res => setUsers(res.users))
  }, [])
	
  return (
    <ul>
      {users.map(user => {
	return <li key={user.id}>{user.name}</li>
      })}
    </ul>
  )
}

Custom Hook을 이용하면 다음과 같이 View와 Logic을 분리할 수 있다.

const UserList = ({ users }) => {
  // Logic
  const users = useGetUserList()
	
  // View
  return (
    <ul>
      {users.map(user => {
	return <li key={user.id}>{user.name}</li>
      })}
    </ul>
  )
}
// Logic - Custom Hook
const useGetUserList = () => {
  const [users, setUsers] = useState([])

  useEffect(() => {
    fetchUsers("/users")
      .then(res => res.json())
      .then(res => setUsers(res.users))
  }, [])

  return users;
}

Custom Hooks 사용법

  • Custom Hook은 이름이 use-로 시작하는 자바스크립트 함수이다. 컴포넌트 바깥에서 선언한다.
  • 컴포넌트로부터 Logic을 분리해내서 Custom Hook 안에 넣어준다. 기존에 컴포넌트 내에서 사용했던 useState, useEffect 등의 Hook들을 Custom Hook 함수 안에서 사용하는 것이다.
  • 컴포넌트의 View를 그리기 위해 필요한 변수 또는 함수들을 Custom Hook 함수에서 return한다.
  • return값을 해당 컴포넌트에서 불러와 View를 그리는 데 사용한다.

같은 Hook을 사용하는 두 개의 컴포넌트는 state를 공유하지 않는다.

두 Custom Hook은 서로 호출되는 위치와 타이밍이 다르며, 애초에 서로 다른 스코프(유효범위)를 생성하기 때문에 컴포넌트를 여러번 호출하는 것처럼 완전히 독립적이다.

Custom Hook.js 파일 분리

Custom Hook은 컴포넌트와는 별도의 파일로 분리해야 한다.

  • 공용으로 쓰이는 Custom Hook은 src/hooks/ 폴더 안에 파일로 생성한다.
  • Custom Hook 하나당 파일 하나를 생성한다(컴포넌트 하나당 파일 하나인 것처럼).
  • Custom Hook 파일명 컨벤션은 따로 없지만 보통 use---.js처럼 함수명으로 한다.

Custom Hooks 코드 예시

다음은 <UserInput> 컴포넌트이다. Logic을 추출해내고 View만 남은 모습이다.

// UserInput.js

const UserInput = () => {
  const { userInfo, handleUserInput } = useUserInput()
  const { size, position } = useDocumentResize()

  return (
    <div>
      <input name="username" onChange={handleUserInput} />
      <input name="id" onChange={handleUserInput} />
      <input name="password" onChange={handleUserInput} />
      <input name="email" onChange={handleUserInput} />
    <div/>
  )
}

다음은 Custom Hooks로 분리해낸 Logic이다.
유저 정보와 관련된 useUserInput과 화면 크기 조절 시마다 문서의 크기와 위치를 리턴하는 useDocumentResize 두 가지 Logic으로 분리되어 있다.

// useUserInput.js

const useUserInput = () => {
  const [userInfo, setUserInfo] = useState({
    username: "",
    id: "",
    password: "",
    email: ""
  })

  const handleUserInput = (e) => {
    const { name, value } = e.target
		
    setUserInfo(prev => ({ ...prev, [name]: value }))
  }

  return { userInfo, handleUserInput }
}
// useDocumentResize.js

const useDocumentResize = () => {
  const [size, setSize] = useState({ width: 0, height: 0 })
  const [position, setPosition] = useState({ width: 0, height: 0 })

  useEffect(() => {
    const handleDocumentSize = () => { ... }

    document.addEventListener("resize", handleDocumentSize)
    return () => {
      document.removeEventListener("resize", handleDocumentSize)
    }
  }, [])

  useEffect(() => {
    const handleDocumentPosition = () => { ... }

    document.addEventListener("resize", handleDocumentPosition)
    return () => {
      document.removeEventListener("resize", handleDocumentPosition)
    }
  }, [])

  return { size, position };
}
profile
공부 기록
post-custom-banner

0개의 댓글