[deep-frontend] 리액트 사이드 이펙트

Zoey·2023년 11월 9일
0

리액트 사이트 이펙트 = useEffect?

리액트 컴포넌트의 순수성

리액트는 컴포넌트가 순수하다고 가정하고 있다.

ex) 순수한 컴포넌트

function Info({name}) {
	return (
    	<ol>
      		<li>{name}는 프론트 엔드 개발자다.</li>
      		<li>{name}는 리액트를 좋아한다.</li>
			<li>{name}는 아침마다 운동하는 걸 좋아한다.</li>
			<li>{name}는 아아를 좋아한다.</li>
      	</ol>
    )
}

export default function App() {
	return (
    	<section>
      		<h1>프론트엔드 개발자 Zoey는?</h1>
      		<Info name="Zoey" />
      	</section>
    )
}

ex) 순수하지 않은 컴포넌트

let guest = 0;

function Cup() {
	guest = guest + 1;
  	return <h2> Tea cup for guest #{guest} </h2>;
}

export default function teaSet() {
	return (
    	<>
      		<Cup />
      		<Cup />
      		<Cup />
      	</>
    )
}

Side Effect

프로그램의 어떠한 상태를 변경한다.

여기서 guest = guest + 1; 의 상태가 원하는 결과 값으로 나오지 않는다.
Conputer science에서 함수가 결과값 이외에 다른 상태를 변경시킬 때 Side Effect가 있다고 한다.

React Side Effect는 언젠가/어디선가/무언가 바뀌어야 한다면 발생한다.

React의 Side Effect가 많이 일어나는 건?

  • 이벤트 핸들러 (클릭, 타이핑, 스크롤, 포커스)

React에서 정의하는 Effect는?

렌더링 자체로 발생하는 것
렌더링으로 인해 발생하는 Side Effect

React Effect와 사용 방법

1. 선언과 의존성 명시

import { useEffect } from 'react';

function MyComponent() {
	useEffect(() => {
    	// 여기의 코드는 매 렌더링 후에 실행된다
    }, []);
  	
  	useEffect(() => {
    	// 여기의 코드는 첫 렌더링과 a의 값이 변화할 때 실행된다
    }, [a]);
  
  	return <div />;
}

2. Clean up 함수

서버 연결 관련되 함수들은 아래처럼 cleanup 함수를 넣어줄 수 있다.

import { useEffect } from 'react';

function MyComponent() {
	useEffect(() => {
    	const connection = createConnection();
      	connection.connect();
      	return () => {
        	connection.disconnection();
        };
    }, []);
 
 
  	return <div />;
}

Clean up 함수는?

  • 서버와의 지속 연결을 끊을 때
  • Fetch를 하고 이를 삭제하거나 무시할 때
  • setTimeout과 같은 timer 관련 기능을 쓸 때
  • 쓰로틀링이나 디바운싱을 할 때
  • 애니메이션을 적용할 때

다음과 같은 경우에 사용할 수 있다.

Data fetch와 effect

보통은 페이지 로드 하자마자 데이터를 보여줘야 하는 경우 useEffect를 써서 페이지 렌더링이 끝나자 마자 데이터를 불러올 수 있다.

하지만 리액트에서는 이를 권장하지 않는다.

이유는 다음과 같다.
1. Effects는 서버에서 실행되지 않는다.
2. Effect에서 직접 fetch하면 "네트워크 워터풀"이 만들어기 쉽다.
3. Effect에서 직접 fetch하는 것은 일반적으로 데이터를 미리 로드하거나 캐시하지 않는다.

리액트가 권장하는 방식

  1. 클라이언트측 캐시를 사용하거나 직접 구현해라.
  2. 프레임워크를 사용한다면 빌트인 fetch 매커니즘을 사용해라.
  3. React Query, useSWR, React Router 6.4+ 등의 오픈 소스를 활용해라

Effect 올바르게 사용하기

생각해보기

  • 특정 이벤트나 렌더링 이후에 일어나아 하는가?
  • 렌더링을 위해 데이터를 변환하는가?
  • 렌더링 중에 해결할 수 있는가?
function Form() {
	const [firstName, setFirstName] = useState('강');
    const [lastName, setLastName] = useState('송');
    const [fullName, setFullName] = useState('');
  
  	useEffect(() => {
    	setFullName(lastName + firstName);
    }, [firstName, lastName]);
  	...
}

=> 위 코드는 아래처럼 수정 가능하다.

function Form() {
	const [firstName, setFirstName] = useState('강');
    const [lastName, setLastName] = useState('송');
    
  	const fullName = lastName + firstName
}

useEffect를 피할수 있는 상황이면 피해서 코드 작성하기

Props에 의한 Effect

Prop이 변경될 때 모든 state가 초기값으로 재설정 되어야 한다면?

export default function ProfilePage({ userId }) {
	const [comment, setComment] = useState('');
    
  	useEffect(() => {
    	setComment('');
    }, [userId]);
  	// ...
}

=> prop값이 변경될때 comment의 상태값을 삭제하고 싶다면 위에 코드처럼이 아니라 아래처럼 변경가능하다.

export default function ProfilePage({ userId }) {
  	retrun (
    	<Profile
      		userId={userId}
			key={userId}
      	/>
    )
}

function Profile({ userId }) {
	const [comment, setComment] = useState('')
}
  • useEffect를 사용하지 않고 userId를 key값으로 전달하고 comment 상태를 변경시킬 수 있다.

Prop이 변경될 때 일부 state만 조정이 필요하다면?

function List({ items }) {
	const [isReverse, setIsReverse] = useState(false);
  	const [selection, setSelection] = useState(null);
    
  	useEffect(() => {
    	setSelection(null);
    }, [items]);
  	// ...
}

=> preItems State를 만들어서 전상태값을 저장하고 아래 코드처럼 변경 가능하다.

function List({ items }) {
	const [isReverse, setIsReverse] = useState(false);
  	const [selection, setSelection] = useState(null);
  	const [preItems, setPreItems] = useState(items);
    
  	if(items !== preItems) {
    	setPreItems(items)
      	setSelection(null)
    }
  	// ...
}

=> 위 코드가 가독성이 떨어진다 다음과 같이 변경 가능하다.

function List({ items }) {
	const [isReverse, setIsReverse] = useState(false);
  	const [selectionId, setSelectionId] = useState(null);
  	
  	const selection = items.find((item) => item.id === selectedId) ?? null;
  	// ...
}

반응형과 비반응형

Props나 Message와 같은 변경될 수 있는 값을 반응형 값이라고 하고 서버 URL과 같은 완전 정적인 값은 비반응형 값 이라고 한다.

const serverUrl = 'https://localhost:1234'; // 비반응형 값

function ChatRoom({ roomId }){
	const [message, setMessage] = useState(''); // 반응형 값
	//...
}

반응형 값은 렌더링 시 데이터 흐름에 참여한다. 이런 반응형 값을 이용해서 이벤트 핸들러와 Effect를 사용할 수 있다.

그렇다면 반응형 값을 이벤트 핸들러로 관리해야할지 Effect로 관리해야할지를 어떻게 구분할 수 있을까?

위 함수를 예로 들자면 sendMessage함수는 message가 변할때마다 이벤트가 일어나야 할까?
=> 답은 아니다! 사용자가 message를 보내고 싶을 때만 이벤트가 일어나야 할것이다.
일반적으로 위와 같은 로직은 비반응형 로직이라고 한다.

그리고 대체적으로 이런 비반응형 로직을 이벤트 핸들러로 다룬다.

function Example({ message = '' }) {
	const [message2, setMessage2] = useState('');
  
    const click = () => sendUserMessage('message1 or message2');
  
  	return (
    	<>
      		<button onClick={click}>메세지 보내기</button>
			...
      	</>
    );
}

다음과 같은 반응형 로직이 있을 경우

const connection = createConnection(serverUrl, roomId);
connection.connect();

아래와 같이 useEffect를 사용하여 처리한다.

function ChatRoom({serverUrl, roomId}) {
	useEffect(() => {
    	const connection = createConnection(serverUrl, roomId);
		connection.connect();
      	
      	return () => {
        	connection.disconnect()
        };
    }, [roomId, serverUrl]); // 이렇게!
  	
  	return <div>연결!</div>
}

Effect 의존성 제거하기

그렇다면 반응형 값을 비반응형 값으로 바꿔 의존성을 제거해보자

const serverUrl = 'http://api.example.com';

function ChatRoom({ roomId }) {
	useEffect(() => {
    	const connection = createConnection(serverUrl, roomId);
		connection.connect();
      	
      	return () => {
        	connection.disconnect()
        };
    }, [roomId]); // 이제 serverUrl은 비반응형 값이기 때문에 의존성에 추가하지 않아도 된다!
  	
  	return <div>연결!</div>
}

만약 반응형 값을 비반응형 값으로 변경하지 못하는 경우?

const serverUrl = 'http://api.example.com';

function ChatRoom({ roomId, theme }) {
	useEffect(() => {
    	const connection = createConnection(serverUrl, roomId);
      	connection.on('connected', () => {
        	showNofication('Connected!', theme);
        });
		connection.connect();
      	return () => {
        	connection.disconnect()
        };
    }, [roomId, theme]); // 이 경우 theme이 의존성에 추가된다.
  	//...
}

위 경우에서 사용 가능한 "useEffectEvent" => 현재 사용가능하며 실험단계

const serverUrl = 'http://api.example.com';

function ChatRoom({ roomId, theme }) {
  	const onConnected = useEffectEvent(() => {
    	showNofication('Connected!', theme);
      	// 이렇게 반응형 로직을 비반응형 로직으로 동작하게 할 수 있다!
    })
  
	useEffect(() => {
    	const connection = createConnection(serverUrl, roomId);
      	connection.on('connected', () => {
        	onConnected();
        });
		connection.connect();
      	return () => connection.disconnect()
    }, [roomId]); // 따라서 이제 의존성에 theme은 들어가지 않는다.
  	//...
}

useEffectEvent 사용시 주의 사항

  1. Effect 내부에서만 호출할 수 있다.
  2. 다른 컴포넌트나 hook에 전달하면 안된다.

조건에 따라 달리하기

function Example() {
	const [isHovered, setIsHovered] = useState(false);
  	const { isMobile } = useMediaQuery();
  
  	useEffect(
    	() => {
        	// 최초 렌더링 시와 isHover가 변화할 때 들어갈 로직을 넣는다.
        },
      	isMobile ? [] : [isHovered], // 삼항 연산자를 활용해 의존성을 이디어 환경에 따라 변경한다.
    )
}

화면을 그리기 전에 Effect 실행시키기

useEffect

useLayoutEffect

렌더링 -> 화면 그리기 -> useEffect로 인해 렌더링 -> 화면 그리기

function Tooltip() {
	const ref = useRef(null);
  	const [tooltipHeight, setTooltipHeight] = useState(0)
    // 아직 height 값이 0이기 때문에 height가 0인 상태로 화면을 그린다.
    
    useEffect(() => {
    	const { height } = ref.current.getBoundingClientReact();
      	setTooltipHeight(height); // 실제 높이를 구한 후 렌더링을 하고 화면을 다시 그린다.
    }, []);
  
  	//... 아래에 작성될 렌더링 로직에 tooltipHeight를 사용한다.
}

=> 이런 경우 렌더링될 때마다 화면의 깜빡거림이 발생한다.

하지만 useLayoutEffect를 사용한다면?

렌더링 -> useLayoutEffect로 인해 렌더링 -> 화면 그리기

function Tooltip() {
	const ref = useRef(null);
  	const [tooltipHeight, setTooltipHeight] = useState(0)
    // 아직 height 값이 0이다. 하지만 사용자는 height가 0인 화면을 볼 수 없다.
    
    useLayoutEffect(() => {
      	// useLayoutEffect를 사용했기 때문에 렌더링이 일어난 이후 화면을 그리지 않고 아래 작업을 수행한다.
    	const { height } = ref.current.getBoundingClientReact();
      	setTooltipHeight(height); // 실제 높이를 구한 후 렌더링을 하고 화면을 그린다.
    }, []);
  
  	//... 아래에 작성될 렌더링 로직에 tooltipHeight를 사용한다.
}

그렇다면 useLayoutEffect는 좋은 hook일까? 그렇지 않다.

Effect 로직이 길어지면 화면에 그려지는 시간이 늦어진다.
State 변화에 따라 브라우저가 화면을 그리는 것을 차단한다. => 사용자가 이를 알아차리지 못한다.
위와 같은 단점들 때문에 주의해서 써야한다.

DOM이 바꾸기 전에 style 주입하기!

Css-in-js / useInsertionEffect

let isInserted = new Set();

function useCSS(rule) {
	useInsertionEffect(() => {
    	if(!isInserted.has(rule)) {
        	isInserted.add(rule);
          	document.head.appendChild(getStyleForRule(rule));
        }
    });
  	return rule
}

function Button() {
	const className = useCSS('...')
    return <div className={className} />;
}

useInsertionEffect를 사용하면 렌더링 하기도 전에 dom이 변이될때 같이 effect가 일어난다. 예시로 이렇게 버튼 컴포넌트를 만들 때 미리 돔을 주입해주는 형식으로 많이 쓰인다.

profile
프론트엔드 개발자가 되기위해 기록하고 공유하는 Zoey 블로그입니다.

0개의 댓글