본 게시글은 리액트 공식문서인 Synchronizing with Effects 를 보고 정리하였습니다.
useEffect
가 필요한 순간들useEffect
에 대해 공부하기 전, useEffect
가 언제 필요한지에 대한 이해가 있어야
이해가 쉬울 것 같다.
리액트 컴포넌트의 역할은 단순하게 두 가지로 정의 할 수 있다.
하나는 UI
에 트리 구조로 된 노드들을 렌더링 하여 시각화 하는 것
두 번째는 이벤트 핸들러를 부착하여, 사용자와 인터렉션을 주고 받는 것 으로 정의 할 수 있을 것이다.
이 두가지 기능의 조합으로 컴포넌트는 필요한 정보를 UI
에 렌더링 하고, 이벤트에 따른 인터렉션을 추가하여
페이지가 기능 할 수 있게 한다.
컴포넌트는 예상 가능한 이벤트에 대한 결과값으로만 인터렉티브하게 움직여야 한다는 제약을 건다.
예를 들어 물건을 구매하고자 할 때는 구매 버튼 컴포넌트를 클릭한다든지 ,
더 많은 게시글을 보고 싶을 때는 더 보기 버튼 컴포넌트를 클릭하여 더 많은 게시글을 렌더링 하든지 말이다.
이를 통해 개발자는 컴포넌트의 역할을 명확하게 하고 예상 가능하게 하여 개발의 편의성을 얻을 수 있다.
리액트에서는 이벤트 외의 이유로 일어난 인터렉션은 Side Effect
로 정의한다. 구매 버튼을 누르지도 않고 페이지만 렌더링 되었는데 구매가 된다거나 하는 행위는
구매 이벤트가 발생하지 않았는데도 인터렉션이 일어나는 것이기 때문이다.
하지만 가끔씩 우리는 이러한 Side Effect
를 필요로 한다.
채팅창 컴포넌트가 존재 할 때 사용자에게 채팅창을 렌더링 하고, 사용자가 직접 서버와 연결 버튼을 눌러 연결을 시작하기보다
렌더링 이후 자동으로 서버와 연결이 된다면 좋을 것이다.
또는 사용자가 페이지에 들어와 초기 렌더링 이후부터 지속적으로 서버 측에 접속과 관련된 로그를
보내고 싶다는 등 말이다.
이렇게 Commit
단계 이후 이벤트와 상관 없이 발생하는 현상들을 모두 Effect
라 부른다.
Effect
가 필요한 순간들ChatRoom
은 사용자가 서버 연결하기 버튼을 누르는 이벤트 가 발생해야지만 서버와 연결한다.
side effect
를 최대한 지양하기 위함이다.
하지만 이는 너무 답답하다. 렌더링 되자마자 자동으로 서버와 연결을 할 수 있게는 못할까 ?
useEffect
를 이용하면 ChatRoom
이 commit
되어 UI
에 나타난 후
서버와 자동으로 연결한다.
useEffect
의 사용법은 추후에 설명하도록 한다.
예전 컴포넌트에서 Actual DOM
에 접근하기 위해서 useRef
를 이용 할 수 있다고 하였다.
렌더링 되면 중앙에서 서서히 공이 커질 수 있도록 해보고 싶다고 하자
코드만 보면 보면 렌더링 된 후 ballRef
를 가지고 있는 태그의 크기가 서서히 커질 것이라 기대된다.
하지만 오류가 뜬다.
ballRef.current
의 값이 null
이기 때문이란다.
이런 이유가 발생하는 이유는 setTimeout
함수의 호출 시점이
실제 Actual DOM
인 <div className = 'ball' ref = {ballRef}>
가 UI
에 나타나기 이전에도 호출되기 때문이다.
useRef
는render , commit phase
일 때 모두 호출된다.
그래서 Actual DOM
에 컴포넌트의 반환값인 엘리먼트가 존재하지 않기에 ballRef.current = null
이 되기에 이런 일이 발생한다.
우리가 원하는 것은 Actaul DOM
에 엘리먼트가 존재한 후에 발생하기를 기대하는 것이다.
그러면 Commit
단계 이후에 발생하는 useEffect
를 이용해주도록 하자
등등 더 많은 활용 방법들이 존재한다.
useEffect
에 대해 공부하고 나서 생각해보도록 하자
useEffect
사용법Effect
선언하기useEffect
는 두 가지 인수를 받는다.
Commit
단계 이후 발생할 이벤트를 담은 콜백함수인 effect: React.EffectCallback
과
React.DependencyList
타입의 deps
배열을 말이다.
@params effect {React.EffectCallback}
첫 번째 인수인 effect
는 Commit
단계 이후 실행될 코드 블록을 작성한다.
리액트 공식문서에서는 해당 내용을 delayed code block
이라고 표현하더라
commit
단계 이후의 일을 다루기 때문에
effect
는 리액트 컴포넌트와 외부 시스템의 동기화를 가능하게 한다.
리액트 컴포넌트는 실제 UI
상태와 완벽하게 일치 될 수 없다.
리액트 컴포넌트가 먼저 호출 된 후, 그에 대한 결과 값으로 UI
상태가 생성되기 때문이다.
하지만 effect
는 UI
가 생성된 상태 이후를 다루기 때문에
리액트 컴포넌트가 UI
와 동기화를 가능하게 한다.
@params deps {React.DependencyList}
deps
배열은 Effect
와 의존성이 있는 값들을 담고 있는 배열이다.
의존성이 뭐 어쩌구 .. 저쩌구 .. ?
useEffect
는 기본적으로 컴포넌트가 호출 된 후 Commit
단계가 지나면 항상 호출된다.
하지만 생각해보자
컴포넌트는 상태가 변경될 때 마다 항상 호출되어 re-rendering
된다.
위에서 들었던 예시로 채팅룸을 기준으로
채팅룸이 렌더링 되고 나면 서버와 연결이 되도록 useEffect
를 사용했다고 해보자
서버와 연결이 완료 되어도 계속하여 서버와 연결을 시도하는 모습을 볼 수 있다.
이런 일이 왜 발생할까 ?
useEffect
내부에서 state
중 하나인 connection
의 상태가 계속 변경됨에 따라
상태 변경에 의한 리렌더링 -> useEffect
재호출 -> 상태 변경에 의한 리렌더링 ... 등의 과정이 반복적으로 발생하는 것이다.
ㅋㅋ 아 ~ 그럼
useEffect
에서 상태를 한 번만 변경 시키면 되는거 아니냐고
하지만 이 또한 문제가 있다.
컴포넌트의 다른 상태가 변경되며, 컴포넌트가 재호출 될 때
마찬가지로 useEffect
가 재호출되면서 서버와 연결을 항상 시도하는 것이다.
우리는 서버의 상태가 변경되지 않는 이상 재연결을 하고 싶지 않다.
사용자가 입력 할 때 마다 서버와 연결을 시도한다면 서버 입장에서는 골머리를 앓을 것이며
페이지 자체도 매우 느리게 될 것이다.
이 ! 때 ~ 의존성을 설정해보세유
우리는 dpes
배열 내에서 useEffect
가 재호출 될 조건을 정해줄 수 있다.
이전에서는 firstConnection
일 경우에만 useEffect
가 재호출 될 수 있도록 하였다.
조건에 따라 useEffect
를 호출하는 것처럼 의존성을 설정 할 수 있다.
deps
배열에 값을 담는 것은 컴포넌트가 재호출되더라도 dpes
배열 내 값이 변하지 않으면 useEffect
를 호출하지 마세요 를 의미한다.
위 예시에선 렌더링이 재호출되더라도 connection
의 상태가 변경되지 않았기에 useEffect
의 effect callback function
이 실행되지 않는 모습을 볼 수 있다.
deps
배열에 아무런 값도 넣지 않았을 경우
deps = []
처럼 아무런 값을 넣지 않으면useEffect
는 첫 렌더링 시에만 호출된다.
하지만deps
를 설정해주지 않으면 매 렌더링 때 마다 호출된다.
cleanup method
설정하기cleanup method
는 useEffect
의 effect callback function
의 반환값인 함수이다.
useEffect
는 컴포넌트가 처음 마운트 되거나 업데이트 된 이후에 시행된다.
클린업 메소드는 컴포넌트가 디마운트 되거나, 업데이트 되기 전 디마운트 될 때 시행된다.
정리하자면 클린업 메소드는 리렌더링이 일어나거나 제거되기 전 시행된다.
클린업 메소드가 필요한 경우를 살펴보자
위에서는 show , hide watch
를 클릭하면 <StomWatch />
컴포넌트가 mount , demount
를 반복하며
useEffect
가 항상 재실행된다.
useEffect
내부의effect callback function
에선deps
를startTime
으로 해놨기에elapsedTime
이 바뀌어도 재실행 되지 않는다.
useEffect
가 일어날 때 마다 인터벌이 한 개가 아닌 두 개씩 쌓이는 이유는<StricMode />
에서
시행하고 있기 때문이다. 이에 대한 내용은 추후 다룬다.
이 때 useEffect
가 기존 컴포넌트를 demount
하고 새로운 컴포넌트를 mount
하더라도
기존에 설정한 setInterval
이 제거되지 않고 있는 모습을 볼 수 있다.
이는 렌더링 되는 화면에는 영향을 미치지 않지만 심각한 페이지의 성능 저하를 불러올 수 있다.
이 뿐만이 아니라 클린업 메소드는 StricMode
에서 페이지를 운영 할 때 더욱 필요하다.
StricMode
에서는 컴포넌트가 렌더링 될 때 의도적으로 렌더링을 두 번 시행한다.
mount -> demount -> mount
로 말이다.
이렇게 하는 이유는 컴포넌트는 항상 같은 값을 렌더링 할 때 마다 같은 결과를 보여야 하는데
렌더링 할 때 마다 결과가 달라지는, pure
하지 못한 컴포넌트임을 감지 할 수 있도록 도와준다.
그런데 생각해보자, useEffect
는 mount
될 때 마다 시행되는데, mount
될 때 마다 무엇인가 지속적으로 쌓인다면 이는 좋은 예시가 아니다.
우리는 demount
될 때 mount
되었을 때 쌓인 인터벌을 제거하고 싶다.
이는 간단히 해결 할 수 있다.
effect callback function
의 반환값으로 demount
됐을 때 시행되길 원하는 코드를
함수로 작성해주면 된다.
클린업 메소드를 이용하면 mount
됐을 때 설정된 인터벌을 제거한다.
이를 통해 mount
될 때 영향을 미친 effect
가 새롭게 mount
됐을 때에 영향을 미치지 않도록 관리해줄 수 있다.
useEffect
는 렌더링 될 때 두번씩 호출될까리액트에서 useEffect
가 두 번씩 발생하는걸 막고 싶어 하는 경우가 많다.
useEffect
가 두 번씩 발생하는 것은 위에서 말했던 SticMode
에서 발생하고 있기 때문이다.
공식문서에서 useEffect
가 두 번씩 발생하는 것을 막기 위해 StricMode
를 사용하지 않는 것을 규탄한다.
StricMode
는 컴포넌트가 pure
한지 알 수 있게 해주는 강력한 도구이기에
StricMode
를 사용하지 않기 보다, 어떻게 해야 useEffect
가 두 번 mount
되어도
한 번만 mount
된 것 처럼 할 수 있을지를 고민하라고 한다.
그 방법이 클린업 메소드를 이용하는 것이다.
컴포넌트가 mount
된 이후 이벤트 핸들러를 부착하기 위해 useEffect
를 사용하면
이벤트 핸들러가 두 번 부착된다.
이를 방지하기 위해 이벤트 핸들러를 제거해주는 클린업 메소드를 이용하자
mount
된 후 애니메이션 효과를 주고 싶어 사용할 경우에는
이전 스타일 -> 이후 스타일 로 변경을 시켜 애니메이션 효과를 구현한다.
이 때 리마운트 됐을 때에도 애니메이션 효과를 위해, demount
됐을 때에는 이전 스타일 로 변경시키도록 한다.
fetch API
위의 코드는 userId
가 변할 때에만 발생하는 userEffect
이다.
userId
별 글 등을 불러오는 거라 생각해보자
이미 보낸 request
를 취소하는 방법은 없기 때문에
어떤 flag
값을 이용해 , flag
가 아닐 때에만 상태를 변경해주도록 하자
하지만 결국
request
를 두 번씩 보내는 것은 동일하다.
이를 방지하고 한 번만 보내고 싶다면 다른 라이브러리를 이용하여 요청을 보내도록 하자커스텀 훅을 이용하자
하지만 웬만해서
useEffect
에서fetch
해오지는 말자그 이유는 다음과 같다.
network waterfalls
useEffect
에서 네트워크 요청을 보내는 경우, 컴포넌트가 리렌더링 될 때 마다 요청을 보내기 때문에 (상태를 변경시키지 않는다 하더라도 ) 이는 불필요한 요청을 계속하여 보내게 된다. 이는 페이지 성능의 악화를 불러 올 수 있다.
- 컴포넌트 재사용성 악화
컴포넌트 내에서 특정
API
로 요청을 보내는 경우에, 해당API
목적에 강한 의존성을 보여 단일 목적으로만 사용되게 된다.예를 들어
GET , POST , PUT , DELETE
후 실행 결과를 로그하는 컴포넌트가 존재한다고 가정했을 때 4가지 컴포넌트를 만드는 것 보다 실행 결과만 로그하는 컴포넌트 하나를 만들고 실행 결과를props
로 건내주는 편이 훨씬 좋을 것이다.
- 컴포넌트의 관심사 분리
2 에서 이야기 한 것과 같은 의미로 컴포넌트는 주로 UI를 렌더링 하는데 사용된다. 컴포넌트가
props
를 받아 렌더링에만 집중 할 수 있도록 할 수록 컴포넌트를 재사용 하거나, 관리하는 것이 편해진다.네트워크 요청과 같은 비즈니스 로직은 컴포넌트 외부의 서비스나 헬퍼 함수에서 처리하는 것이 바람직하다.
설명하시는 코드들은 모두 직접 구현하시는 건가요?