주니어 개발자를 위한 react hook 필수 개념 5가지

Q&A 큐레이션

1. useEffect 관리하는 방법?

안녕하세요. react를 접한지 얼마 안 된 신입 개발자인데요. 기존 코드를 보고있는데 한 컴포넌트에 useEffect가 10 몇개씩 사용되고있는데, 이런식으로 사용하는게 맞는지 궁금합니다. 선언된 state를 보다가 state의 setter를 따라서 useEffect를 들어가다보면 어느샌가 코드 파일을 위 아래로 스크롤만 하고있고 코드가 뭐하는지 이해하기가 좀 어렵더라구요. 혹시 이렇게 비대한 컴포넌트를 이해하기 쉽게 effect를 관리하는 방법이 있나요? 또는 참고자료가 있다면 알려주세요. 감사합니다!


답변

코드를 보지 못해서 정확한 답변은 드리기 어렵지만 한 컴포넌트에 useEffect가 10개가 넘는게 일반적인 경우는 아닙니다. React.js 공식 문서의 조언에 의하면 useEffect는 마지막 수단으로 사용되어야 합니다. (https://beta.reactjs.org/learn/keeping-components-pure#where-you-_can_-cause-side-effects) useEffect를 제거할 때 고려해볼 수 있는 케이스는 아래와 같습니다. 1. API 요청을 통해 데이터를 불러오는 경우 데이터 요청 로직을 useEffect에서 할 수도 있지만, React Query같은 서버 상태 관리 도구를 쓰면 useEffect를 제거하고 간결한 코드를 유지할 수 있습니다. 2. 상태에 따른 부수효과를 처리하는 경우 이벤트 핸들러(onClick, onSubmit 등)에서 처리할 수 있는 로직을 useEffect로 처리하는 경우 이벤트 핸들러로 로직을 옮기는 것을 고려해볼 수 있습니다. 이벤트 핸들러에서 처리할 수 없는 로직인 경우 상태 설계를 잘못했는지 고려해봐야 합니다. 신입 개발자시라면 잘 이해가 안되실수도 있습니다. 아래 글들을 읽으면서 useEffect에 대한 개념을 먼저 정립해보시는걸 추천드립니다. useEffect 완벽 가이드 - https://overreacted.io/ko/a-complete-guide-to-useeffect/ useEffect가 나를 열받게 했다 - https://iborymagic.tistory.com/140

외 1개 답변

모두 보기

2. useLayoutEffect는 언제 쓰는건가요?

안녕하세요~ 이번에 코드 리뷰를 받았는데 "useEffect 대신 useLayoutEffect를 쓰는게 좋아요~" 라는 피드백을 받았습니다. 코드 구조는 컴포넌트에서 ref를 선언한 후 useEffect로 해당 값을 바꾸고, 추후 다른 컴포넌트의 훅에서 이 ref 값을 참조해서 로직이 돌아가는 형태였습니다 (회사 코드라 첨부를 할 수가 없네요...ㅠㅜ) 그런데 왜 useEffect 대신에 useLayoutEffect를 써야하는지 잘 모르겠더라구요. 혹시 이유를 아시는분 있나요?


답변

안녕하세요! 음, 코드가 어떤지 몰라서 정확한 이유는 알 수 없지만 아마도 렌더 직후, 브라우저가 새로운 창을 그리기 전에 돌아가야하는 로직이여서 그런 것 아닐까요? useEffect는 브라우저가 스크린을 보여주고 난 뒤에 실행되고 useLayoutEffect는 React가 DOM을 업데이트하고 브라우저가 스크린을 다시 그리기 전에 실행됩니다. hooks-flow 한번 참고해보세요!

모두 보기

3. useEffect 순서 관련 질문!

안녕하세요. useEffect의 순서는 parent -> child 순으로 된다고 알고 있는데 왜 그런지 혹시 설명해주실분 계신가요? 직접 console.log로 찍어보면 순서가 잘 돌아가는 것은 알겠는데 왜 이런 순서가 보장되는 것인지 이해가 잘 안되더라구요. 감사합니다!


답변

오잉 반대아닌가요? child -> parent 순인 것 같습니다. 간단히 설명하자면 작성하신 react 컴포넌트가 화면에 그려지기까지 여러 단계를 거치게 되는데요. 크게 render phase와 commit phase가 있습니다. render phase에서는 작성된 코드를 기반으로 어떻게 DOM을 수정해야할지 계산하는 단계고 commit phase에서는 실제 수정을 가하는 단계라고 생각하시면 될 것 같습니다. "useEffect의 순서가 어떻게 보장되나요?" 에 대한 답은 react가 작업 순서를 어떻게 정하는지 보면 될 것 같습니다. react는 각 컴포넌트를 react fiber node라는 객체로 변환을해서 render, commit phase를 수행하게 됩니다. react fiber node들은 linked list 형태로 저장이 되어 있어서 root fiber node에서 child fiber node까지의 순서가 보장됩니다. 비슷하게 useEffect도 effect list에 저장이되고 commit 단계에서 해당 effect list를 돌면서 useEffect를 수행하게 됩니다. 즉, 질문자님의 코드는 react에 의해서 아래의 과정을 거치게 될 것 같습니다. (편의를 위해 1단계 2단계로 나누겠습니다. render, commit phase와는 관계 없음) 1 단계: react fiber node 생성 단계와 effect list에 useEffect를 담는 과정 - App의 render 함수를 보니 ChildComponent라는 react fiber node를 생성하는 함수임. - ChildComponent의 render 함수를 보니 GrandChildComponent라는 react fiber node를 생성하는 함수임. - GrandChildComponent라는 react fiber node를 생성하면서 해당 컴포넌트 컨텍스트에서 돌아가는 코드 (useEffect와 기타 코드)를 보고 여러가지 메타 정보 및 commit 단계에서 실행해야할 함수를 react fiber node 객체에 저장함. useEffect의 경우는 hook이라는 객체로 생성이되고 effect list (작업 큐)에 담김 - ChildComponent도 위와 마찬가지 - App도 위와 마찬가지 1단계 종료시: - react fiber node는 App (root) -> ChildComponent (child node) -> GrandChildComponent (child's child node) 형태로 형성됨 - effect list는 [GrandChildComponent's useEffect, ChildComponent's useEffect, App's useEffect] 형태로 형성됨 2 단계: 작업 처리 과정 - App이라는 root fiber node를 기준으로 작업을 처리하기 시작함 - App의 render 함수를 실행하려고 보니 ChildComponent를 먼저 render 해야함. - ChildComponent도 GrandChildComponent를 먼저 render 해야함. - GrandChildComponent를 먼저 렌더하고, 해당 컨텍스트에 저장된 코드를 수행함 - ChildComponent도 위와 마찬가지 - App도 위와 마찬가지 - 렌더 후 effect list를 돌면서 effect들을 수행함 - effect list에 담긴 순서가 GrandChildComponent -> ChildComponent -> App 순이니 child -> parent 순으로 console.log 찍힘 저도 리액트 내부를 정확히 다 이해하고 있는 것은 아니니 대충 이런 느낌이다 정도로 참고하시면 될 것 같습니다. 더 정확한건 react source code를 직접보시거나 react fiber와 render, commit 단계를 설명해놓은 블로그들을 참고하시게 좋아보여요! 몇개 첨부해놓겠습니다 - https://www.bussieck.com/useeffect-under-the-hood/ - https://blog.logrocket.com/deep-dive-react-fiber/ - https://github.com/facebook/react - https://beta.reactjs.org/learn/render-and-commit

모두 보기

4. react의 setState가 정상동작하지 않아요.

react를 이용해 이것저것 테스트해보고 있는데요. 버튼을 눌렀을 때 state를 1 증가시키는 예제를 해보고 있습니다. const oneCountUp = () => { setCount(count + 1); }; 위처럼 1 증가시키는 함수를 정의해서 onClick에서 위의 함수를 호출하는 방식으로 했습니다. 근데 호기심에 3을 증가시켜보자 하고 oneCountUp을 3번 호출했습니다. 근데 여전히 1만 올라갑니다. 왜 그런걸까요??? import React, { useState } from 'react'; export default function CountTest() { const [count, setCount] = useState(1); const oneCountUp = () => { setCount(count + 1); }; return ( <div> <button onClick={() => { oneCountUp(); oneCountUp(); oneCountUp(); }} > 1 증가 </button> <p>{count}</p> </div> ); }


답변

혹시 공식문서의 https://ko.reactjs.org/docs/faq-state.html#why-is-setstate-giving-me-the-wrong-value 이 부분을 보셨나요? 클래스 컴포넌트로 되어있긴한데 매우 유사한 상황인것 같습니다. 'setState 호출은 비동기적으로 이뤄집니다. 따라서 setState 호출 직후 새로운 값이 state 에 반영될 거라고 믿어서는 안 됩니다.' 라고 적혀있습니다. oneCountUp(); oneCountUp(); oneCountUp(); 이렇게 3번 호출 할 때 순차적으로 호출되고 끝나고 호출되고 끝나고가 아닌 비동기로 이루어져서 전부 현재 상태에서 1을 증가시키게 되는것으로 보입니다. 이걸 수정하려면 oneCountUp함수를 수정해야할 것 같아요 setState함수에는 값 말고 함수를 넘겨줄 수 있습니다. 그리고 함수의 첫번째 인자로 직전 state를 가지고 오게 됩니다. 그래서 아래처럼 변경하면 될 것 같네요 const oneCountUp = () => { setCount((beforeCount) => beforeCount + 1); };

모두 보기

5. useRef를 이용하는 것과 그냥 변수를 정의하는 것의 차이는 무엇인가요?

useRef를 통해 특정 element를 선택할 수도 있지만 값을 넣어 사용하는 경우도 있다고 하는데 그냥 변수로 사용하는 것과 어떤차이가 있나요? 아래 두 코드의 차이점이 궁금합니다. export default function Test () { const a = useRef(0) } export default function Test () { const a = 0 }


답변

useRef는 DOM element를 저장하는 것 뿐만 아니라 질문주신대로 number를 비롯해 어떤 값이든 저장할 수 있습니다. 이를 저장할 수 있는 배경은 사실 useRef()가 사실 순수 Javascript 객체를 생성하기 때문인데요! 질문에 있는 값이 0이 0 고대로 저장되는 것이 아니라 { current: 0, ... } 같이 객체의 형태로 값이 관리됩니다. 그러면 const a = { current: 0 } 이랑은 무슨 차이가 있느냐고 말씀하실 수 있는데 React 공식 문서의 말을 빌리자면 객체 자체를 생성하는 것의 유일한 차이점은 useRef는 매번 렌더링을 할 때 동일한 ref 객체를 제공한다는 것입니다. const a = 0 이나 const a = { current: 0 } 의 경우는 렌더링될 때마다 값을 초기화 합니다. 따라서 컴포넌트의 라이프사이클동안 관리해야하는 변수에는 적절하지 않습니다. + 마지막 TMI로 useRef에서 생성하는 객체는 heap 영역에 저장되므로 웹앱이 종료되거나 가비지 컬렉팅되기 전까지 참조할 때마다 동일한 메모리 값을 가집니다. 참고. React 공식문서 useRef 섹션 https://ko.reactjs.org/docs/hooks-reference.html#useref

외 1개 답변

모두 보기