useEffect ⚛️

대신 설명해 드립니다

발표자 소개

안도형

프로토파이 클라우드 서비스
프론트엔드 개발합니다
(동료 모집 중)

오늘은 시간이 없어서 여기까지

좀 더 궁금하시다면

🐦 twitter: @adhrinae

리액트 쓰시는 분 ✋

훅을 사용하시는 분 (적극적으로) ✋

공식 문서 훅 부분 정독해 보신 분 ✋

이번 발표는 주무셔도 됩니다

오늘 발표 내용

  • 훅을 이해하고 사용하는데 가장 큰 몫을 차지하는 useEffect 를 알아보자

  • (클래스 컴포넌트에 익숙하다면) 알고 있던 것을 잊어버리는 시간을 가져보자

With Dan Abramov

번역을 한번 해 봤더니

컴포넌트 랜더링(Render)

type Component = (props, state) => UI
// 이게 성립한다면
f(props, state) => UI

// 이것도 성립합니다
f(g(h(props, state))) => UI

// 입력값이 같으면 결과값이 같다는 전제 아래

언제 함수(컴포넌트)가 실행되는가?

(언제 (리)랜더 되는가?)

props 혹은 state가 변경되었을 때

Quiz 🧙‍♂️

  1. f 라는 함수가 실행되면

  2. f 함수 안에 정의되는 값은

  3. 이전에 실행된 f 함수 안에 정의되었던 값과

  4. 관련이 있을까요?

function f(props) {
  const name = props.name
  const genre = props.genre
  const likeCount = props.likeCount

  const makeArtist = () => `Artist name: ${name}, Genre: ${genre}`
  
  return {
    artistInfo: makeArtist(),
    likeCount
  }
}

f({name: 'Michael Jackson', genre: 'Pop', likeCount: 1})
// {artistInfo: 'Artist name: 'Michael Jackson, Genre: Pop', likeCount: 1}

f({name: 'Michael Jackson', genre: 'Pop', likeCount: 2})
// {artistInfo: 'Artist name: 'Michael Jackson, Genre: Pop', likeCount: 2}

f({name: 'Michael Jackson', genre: 'Pop', likeCount: 3})
// {artistInfo: 'Artist name: 'Michael Jackson, Genre: Pop', likeCount: 3}
function Counter() {
  const [count, setCount] = useState(0)

  function handleAlertClick() {
    setTimeout(() => {
      alert('클릭한 횟수: ' + count)
    }, 3000)
  }
  
  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
      <button onClick={handleAlertClick}>
        Show alert
      </button>
    </div>
  )
}
// 처음 랜더링 시
function Counter() {
  const count = 0; // useState() 로부터 리턴  // ...
  function handleAlertClick() {
    setTimeout(() => {
      alert('You clicked on: ' + count);
    }, 3000);
  }
  // ...
}

// 클릭하면 함수가 다시 호출된다
function Counter() {
  const count = 1; // useState() 로부터 리턴  // ...
  function handleAlertClick() {
    setTimeout(() => {
      alert('You clicked on: ' + count);
    }, 3000);
  }
  // ...
}

// 또 한번 클릭하면, 다시 함수가 호출된다
function Counter() {
  const count = 2; // useState() 로부터 리턴  // ...
  function handleAlertClick() {
    setTimeout(() => {
      alert('You clicked on: ' + count);
    }, 3000);
  }
  // ...
}

모든 랜더링마다 고유의 이 존재

그렇다면 useEffect(이펙트)는?

function Counter() {
  const [count, setCount] = useState(0)

  useEffect(() => {
    setTimeout(() => {
      console.log(`You clicked ${count} times`)
    }, 3000)
  })
  
  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  )
}

새로운 랜더링마다
새로운 값을 끌어다 쓴다

클래스 컴포넌트는 조금 다릅니다 😉
(이번 시간에는 다루지 않음)

useEffect → cleanup

useEffect의 첫 실행 시점

  • 브라우저가 페인트 작업을 하고 난 뒤

  • 화면을 그리기 이전에 로직을 실행하고 싶다면? useLayoutEffect

 useEffect(() => {
   // ...
   ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange)
   return function cleanup() {
     ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange)
   }
 })

1. 리랜더링 시작

2. 이전 이펙트에서 담아둔 값으로 cleanup 실행

3. 다음 이펙트 실행

Closure

useEffect !== LifeCycle

class Chart extends Component {
  componentDidMount() {
    // 차트가 마운트되면 실행
  }
  componentDidUpdate(prevProps) {
    if (prevProps.data == props.data) return
    // data 속성이 업데이트 되었다면 실행
  }
  componentWillUnmount() {
    // 차트가 언마운트 되기 전에 실행
  }
  render() {
    return (
      <svg className="Chart" />
    )
  }
}
const Chart = ({ data }) => {
  useEffect(() => {
    // 차트가 마운트되면 실행
    // 차트가 업데이트되면 실행
    return () => {
      // 데이터가 업데이트되면 실행
      // 차트가 언마운트되기 전에 실행
    }
  }, [data])
  
  return (
    <svg className="Chart" />
  )
}

컴포넌트 내부/외부의 요소를 동기화

useEffect를
매번 실행할 필요가 없는데요?

function Greeting({ name }) {
  // props, state 가 변할 때마다 실행된다.
  useEffect(() => {
    document.title = 'Hello, ' + name
  })
  
  return (
    <h1 className="Greeting">
      Hello, {name}
    </h1>
  )
}
useEffect(fn, deps)

두 번째 인자는 어떤 역할을 하는가?

리액트 자체의 예를 들자면

<h1 className="hello">
  Hello, Dohyung
</h1>
// { className: 'hello', children: 'Hello, Dohyung' }

<h1 className="hello">
  Hello, Lance
</h1>
// { className: 'hello', children: 'Hello, Lance' }

children 부분만 바뀌었으니 children만 업데이트

리액트는 실제로 바뀐 부분만
DOM 업데이트를 합니다

Effect의 비교?

useEffect(() => {
  document.title = 'Hello, ' + name
})
// () => { document.title = 'Hello, Dohyung' }

useEffect(() => {
  document.title = 'Hello, ' + name
})
// () => { document.title = 'Hello, Lance' }

두 함수의 달라진 부분을 어떻게 직접 잡아낼 수 있나?

직접 알려줍시다, 배열로.

useEffect(() => {
  document.title = 'Hello, ' + name
}, [name])

이펙트를 한 번만 실행하고 싶다면?

빈 배열을 전달한다.

deps(의존성)을 솔직하게 알려주자

function Counter() {
  const [count, setCount] = useState(0)

  useEffect(() => {
    const id = setInterval(() => {
      setCount(count + 1)
    }, 1000)
    
    return () => clearInterval(id)
  }, [])
  
  return <h1>{count}</h1>
}
// 실제로 일어나는 일
function Counter() {
  // ...

  useEffect(() => {
    const id = setInterval(() => {
      setCount(0 + 1) // 계속 0 반복
    }, 1000)
    
    return () => clearInterval(id)
  }, [])
  
  // ...
}

원하는 동작을 구현해보고 싶다면
일단 의존성을 곧이곧대로 넣어보기

의존성을 다르게 다루는 방법

카운터를 동작하게 하는데
정말 count 값이 필요했을까요?
setState(prevState => nextState)
function Counter() {
  const [count, setCount] = useState(0)

  useEffect(() => {
    const id = setInterval(() => {
      setCount(c => c + 1)
    }, 1000)
    
    return () => clearInterval(id)
  }, [])
  
  return <h1>{count}</h1>
}

새 상태를 직접 전달하지 않고
상태가 어떻게 바뀌어야 하는지
지침을 전달

여러 값을 동시에 다루어야 할 때는
한계에 부딪힘

useEffect 를 다루는 치트키

useReducer

function Counter() {
  const [count, setCount] = useState(0);
  const [step, setStep] = useState(1);

  useEffect(() => {
    const id = setInterval(() => {
      setCount(c => c + step)
    }, 1000)
    
    return () => clearInterval(id)
  }, [step])
  
  return (
    <>
      <h1>{count}</h1>
      <input
        value={step}
        onChange={e => setStep(Number(e.target.value))}
      />
    </>
  )
}
function Counter() {
  const [state, dispatch] = useReducer(reducer, initialState)
  const { count, step } = state

  useEffect(() => {
    const id = setInterval(() => {
      dispatch({type: 'TICK'})
    }, 1000)
    
    return () => clearInterval(id)
  }, [dispatch])
  
  return (
    <>
      <h1>{count}</h1>
      <input
        value={step}
        onChange={
          e => dispatch({
                 type: 'SET_STEP',
                 payload: Number(e.target.value)
               })
        }
      />
    </>
  )
}

컴포넌트를 표현하는 상태와
업데이트 로직을
분리하여 다룰 수 있음

무조건 useReducer 만
쓰실 필요는 없습니다

Data Fetching

컴포넌트가 마운트 될 때만
불러오는 것은 쉽죠

상태 변화와 동기화시키고 싶다면?

function SearchResults() {
  const [query, setQuery] = useState('react');

  useEffect(() => {
    function getFetchUrl() {
      return 'https://hn.algolia.com/api/v1/search?query='
               + query
    }

    async function fetchData() {
      const result = await axios(getFetchUrl());
      setData(result.data);
    }

    fetchData()
  }, [query])
  
  // ...
}

분리 방법

  • 아예 컴포넌트 바깥으로 빼고 외부에서 import
     
  • useCallback으로 감싼다
function SearchResults() {
  const [query, setQuery] = useState('react');

  const getFetchUrl = useCallback(() =>
    'https://hn.algolia.com/api/v1/search?query=' + query
  , [query])
  
  useEffect(() => {
    const url = getFetchUrl();
    // ... 데이터를 불러와서 무언가를 한다 ...
  }, [getFetchUrl])

  // ...
}

함수도 데이터 흐름의 일부가 된다

Date Fetching이 더 궁금하다면

zeit/swr

react-async

useEffect의 미래

  • useEffect는 엄연히 리액트의 저수준 API

  • 리액트 커뮤니티에서
    다양한 방식으로 추상화된 커스텀 훅이 나옴

  • 특수한 목적이 아닌 이상
    useEffect 자체를 쓸 일은 많이 줄어들 것

핵심 다시 요약

  • 컴포넌트는 (props, state) => UI
    라는 타입 정의를 가지고 있는 하나의 함수.

  • 따라서 함수가 새로 실행될 때마다(랜더링 때마다)
    함수 내부에 고유의 값을 가진다.(상수, 함수, 이벤트 등)

  • 일단 린트를 써 보자

  • LifeCycle(X) / Synchronization(O)

참고 자료

감사합니다 🧙‍♂️

Made with Slides.com