본문 바로가기

POSCO x CODINGOn 웹개발자 양성 부트캠프

[포스코x코딩온] 웹개발자 풀스택 과정 13주차 회고 | React : [Ref, useRef(), useMemo(), useCallBack(), useReducer()]

React Ref

기존 HTML에서 고유한 요소를 선택해야 할 경우, 해당 요소에 id를 지정하고 요소를 호출했다.

<div id='content-div'>내용</div>

 

React에서는 HTML 요소들을 component로 재사용을 하기에 특정 요소에 id를 지정되는 component가 재사용될 경우, 해당 id가 동일하게 지정되기에 고유한 값이 될 수 없는 문제가 있다.

 

Ref는 component내부에서만 고유한 값으로 작동하기에 해당 문제를 해결한다.

 

Ref (Reference)

  • 전역적으로 작동하지 않고 component 내부에서 선언 및 사용된다.
  • 동일한 component를 반복 사용해도 각 component 내부에서만 동작하기에 id처럼 중복되지 않는다.
  • DOM을 직접적으로 건드려야 할 때 사용된다.
    • 특정 input에 focus 주기, 스크롤 박스 조작, 비디오 플레이어의 재생/정지 기능 등
  • 가능한 사용을 최소화하는 것이 좋다.
  • 클래스형 component에서만 기본 기능으로 제공한다.

 

클래스형 component에서 Ref 사용 방법

 

<input ref={ (ref) => {this.input = ref} />

<!-- this.input은 원하는 이름으로 지정할 수 있다. -->

 

1. 콜백 함수

  • 사용하고자 하는 DOM 요소에 ref라는 콜백 함수 작성 및 props로 전달한다.
  • ref를 component의 멤버 변수로 설정하는데, 이때 ref는 원하는 대로 사용할 수 있다.
import { Component } from 'react';

//콜백함수 형태
class RefSampleClass1 extends Component {
	handleFocus = () => {
    	//myInput ref가 지정된 input태그 요소에 접근
		this.myInput.focus();
	};

	render() {
		return (
			<>
				<p>(클래스형 컴포넌트) 버튼클릭시 input에 focus 처리</p>
				<input
					type='text'
					ref={(ref) => {
						this.myInput = ref;
					}}
				/>
				<button type='button' onClick={this.handleFocus}>
					focus
				</button>
			</>
		);
	}
}

export default RefSampleClass1;

 

2. 내장 함수 (createRef())

  • React.createRef() 함수를 이용해 component 내부에서 변수에 ref요소를 담아준다.
  • 이때 만든 요소는 실제 DOM 요소의 prop에 연결되어야 한다.
  • 사용 시 ref 요소.current를 이용한다.
import React, { Component } from 'react';

//내장함수 형태 createRef()
class RefSampleClass2 extends Component {
	//createRef를 사용해서 만들때는 component 내부 변수에 React.createRef()를 할당해주어야함
	myInput = React.createRef();

	handleFocus = () => {
		//ref를 설정한 DOM에 접근하기 위해서는 this.myInput.current 사용
		this.myInput.current.focus();
	};

	render() {
		return (
			<>
				<p>(클래스형 컴포넌트) 버튼클릭시 input에 focus 처리</p>
				<input ref={this.myInput} />
				<button onClick={this.handleFocus}>focus</button>
			</>
		);
	}
}

export default RefSampleClass2;

 

함수형 component에서 Ref 사용 방법

 

Hook - useRef()

  • 함수형 component에서 ref를 사용하기 쉽게 만들어주는 Hook
  • useRef()는 인자로 받은 값으로 초기화된 변경 가능한 ref 객체를 반환한다.
  • ref 요소.current로 현재 가리키는 객체에 접근할 수 있다.

 

useRef() 용도

 

1. DOM 요소에 접근

  • ex. 버튼 클릭 시 input 요소에 포커스 주기 => input 요소에 직접 접근이 필요하다.
import { useRef } from 'react';

export default function RefSampleFunction1() {
	// 1. ref 객체 만들기
	const myInput = useRef();

	const handleFocus = () => {
		// 3. useRef()로 만든 객체의 current값에 focus() 적용
		myInput.current.focus();
	};
	return (
		<>
			<p>(함수형 컴포넌트) 버튼 클릭시 input에 focus 처리</p>
			{/* 2. 선택하고 싶은 DOM에 ref prop 설정 */}
			<input ref={myInput} />
			<button onClick={handleFocus}>focus</button>
		</>
	);
}

 

2. 로컬변수로 사용

  • 로컬변수는 재랜더링되어도 기존 값이 그대로 유지된다.
import { useRef, useState, useEffect } from 'react';

//로컬변수 - 렌더링되어도 값을 그대로 유지
export default function RefSampleFunction2() {
	const [time, setTime] = useState(0);

	const idRef = useRef(0);

	const plusIdRef = () => {
		setTime(0);
		idRef.current += 1; //로컬 변수 값 변경
		console.log(idRef.current);
	};

	useEffect(() => {
		const interval = setInterval(() => {
			setTime((prev) => prev + 1);
		}, 1000);

		//setInterval에 설정된 반복을 취소
		return () => clearInterval(interval);
	}, []);

	return (
		<>
			<h1>Ref Sample</h1>
			{/* 1. useRef()로 생성된 객체는 .current 프로퍼티를 가지고 있어 이를 통해 변수에 접근가능 */}
			{/* 2. useRef()로 생성된 변수는 컴포넌트가 리랜더링 되어도 그 값을 유지 */}
			{/* 3. useRef()의 값이 변경되어도 컴포넌트는 리랜더링되지 않음 */}
			<h2>{time}</h2>
			<h2>{idRef.current}</h2>
			<button onClick={plusIdRef}>PLUS Ref</button>
		</>
	);
}

Hook - useMemo()

  • 메모이제이션을 통해 함수의 리턴 값을 재사용할 수 있게 해주는 Hook
  • 함수형 component 내부에서 발생하는 연산을 최적화시켜 준다.
  • 렌더링 과정에서 특정 값이 바뀌었을 때만 연산을 실행한다.
  • 렌더링 과정에서 두 번째 인자로 받은 의존 배열(dependency) 내 값이 바뀌는 경우에만 첫 번째 인자로 받은 콜백함수를 실행해 구한 값을 반환한다.
const memoizedValue = useMemo(callback, dependency);

useMemo() 예제

import { useState, useMemo } from 'react';

export default function SelfStudyUseMemo() {
	const [count1, setCount1] = useState(0);
	const [count2, setCount2] = useState(0);

	const handleAdd1 = () => {
		setCount1((prev) => prev + 1);
	};

	const handleAdd2 = () => {
		setCount2((prev) => prev + 1);
	};

	const count1Squared = count1 * count1;
	console.log('count1Squared', count1Squared);

	return (
		<>
			<div>count1: {val1}</div>
			<div>count2: {val2}</div>
			<div>count1 * count1: {count1Squared}</div>
			<br />
			<button type='button' onClick={handleAdd1}>
				Add count1
			</button>
			<button type='button' onClick={handleAdd2}>
				Add count2
			</button>
		</>
	);
}

위 코드는 count1과 count2라는 상태가 있으며 각각 이전 상태 값에 1씩 증가시키는 버튼이 있다. count1Squared 변수의 값은 count2의 값으로부터 어떠한 영향을 받지 않음에도 불구하고 count2 상태가 변경될 경우, component는 다시 렌더링 되며 결과적으로 count1Squared이라는 변수의 값도 불필요하게 다시 연산이 된다.

 

이 상황에서 useMemo()를 사용할 경우, 아래 코드와 같이 의존 배열 안에 있는 count1 상태의 값이 변경될 경우에만 count1Squared의 연산이 다시 실행된다.

 

import { useState, useMemo } from 'react';

export default function SelfStudyUseMemo() {
	const [count1, setCount1] = useState(0);
	const [count2, setCount2] = useState(0);

	const handleAdd1 = () => {
		setCount1((prev) => prev + 1);
	};

	const handleAdd2 = () => {
		setCount2((prev) => prev + 1);
	};

	const count1Squared = useMemo(() => {
		console.log('count1Squared', count1Squared);
		return count1 * count1;
	}, [count1]);

	// const count1Squared = count1 * count1;
	// console.log('count1Squared', count1Squared);

	return (
		<>
			<div>count1: {count1}</div>
			<div>count2: {count2}</div>
			<div>count1Squared: {count1Squared}</div>
			<br />
			<button type='button' onClick={handleAdd1}>
				Add count1
			</button>
			<button type='button' onClick={handleAdd2}>
				Add count2
			</button>
		</>
	);
}

Hook - useCallback()

  • 메모이제이션을 통해 함수를 재사용할 수 있게 해주는 Hook
  • 함수를 다시 불러오는 것을 막으며 렌더링 최적화에 사용된다.
const memoizedCallback = useCallback(callback, dependency);

useCallback() 예제

import { useEffect, useState, useCallback } from 'react';

export default function SelfStudyUseCallback() {
	const [number, setNumber] = useState(0);
	const [toggle, setToggle] = useState(true);

	const someFunc = () => {
		console.log(`someFunc: number: ${number}`);
		return;
	};

	useEffect(() => {
		console.log('someFunction이 변경되었습니다.');
	}, [someFunc]);

	return (
		<>
			<div>
				<input type='number' value={number} onChange={(e) => setNumber(e.target.value)} />
				<button onClick={() => setToggle(!toggle)}>{toggle.toString()}</button>
				<br />
				<button onClick={someFunc}>someFunc</button>
			</div>
		</>
	);
}

위 코드는 number와 toggle 상태가 있으며 number상태를 변경할 수 있는 input과 toggle상태를 변경할 수 있는 button과 someFunc함수를 호출하는 button이 있다. useEffect를 사용하여 someFunc가 변경될 경우에만 someFunction이 변경되었다는 내용을 콘솔에 찍으려고 하지만, toggle상태만 변경해도 useEffect가 실행되는 것을 볼 수 있다. 함수형 component는 어떠한 상태가 변경되면 해당 함수 안에 있는 변수들을 모두 초기화하는데, 이때 someFunc라는 변수에 할당되는 함수도 같이 초기화가 된다.

 

만약 toggle상태만 변경될 경우에 someFunc함수의 초기화를 막고, number상태만 변경될 경우에만 someFunc함수를 초기화시키고 싶다면, 아래 코드처럼 number상태를 의존성 배열에 추가하여 useCallback()을 사용할 수 있다.

import { useEffect, useState, useCallback } from 'react';

export default function SelfStudyUseCallback() {
	const [number, setNumber] = useState(0);
	const [toggle, setToggle] = useState(true);

	const someFunc = useCallback(() => {
	 	console.log(`someFunc: number: ${number}`);
	 	return;
	}, [number]);

	useEffect(() => {
		console.log('someFunction이 변경되었습니다.');
	}, [someFunc]);

	return (
		<>
			<div>
				<input type='number' value={number} onChange={(e) => setNumber(e.target.value)} />
				<button onClick={() => setToggle(!toggle)}>{toggle.toString()}</button>
				<br />
				<button onClick={someFunc}>someFunc</button>
			</div>
		</>
	);
}

Hook - useReducer()

  • 복잡한 component 상태 로직을 reducer() 함수를 통해 관리할 수 있는 Hook
  • 현재 상태와 업데이트를 위해 필요한 정보를 담은 액션 값을 전달받아 새로운 상태를 반환하는 함수
  • component 업데이트 로직을 component 외부로 뺄 수 있다
  • useState()의 대체 함수로 다양한 component 상황에 따라 상태값을 설정할 수 있다

useReducer() 과  useState() 예제

import { useReducer, useState } from 'react';

//초기값
const initialState = { count: 0 };

//reducer함수(상태, 액션)을 받아 새로운 상태를 반환하는 함수
//reducer(state, action), 액션에는 type이 존재
function reducer(state, action) {
	switch (action.type) {
		case 'INCREMENT':
			return { count: state.count + 1 };
		case 'DECREMENT':
			return { count: state.count - 1 };
		default:
			throw new Error('error');
	}
}

export default function Counter() {
	//reducer: state를 업데이트하는 함수
	//state: 현재 상태
	//dispatch: 액션을 발생시키는 함수
	const [state, dispatch] = useReducer(reducer, initialState);

	const [stateB, setStateB] = useState({ count: 0 });

	return (
		<div>
			<p>useReducer count: {state.count}</p>
			<button onClick={() => dispatch({ type: 'INCREMENT' })}>Increment</button>
			<button onClick={() => dispatch({ type: 'DECREMENT' })}>Decrement</button>
			<br />
			<p>useState count: {stateB.count}</p>
			<button onClick={() => setStateB({ count: stateB.count + 1 })}>PLUS</button>
			<button onClick={() => setStateB({ count: stateB.count - 1 })}>MINUS</button>
		</div>
	);
}

느낀 점들

메모이제이션이라는 컴퓨터 공학적인 개념을 처음에는 이해하기 어려웠으나, 여러 강의 영상들을 보며 완전한 이해를 할 수 있었다. useMemo()와 useCallback()은 개발 초기 단계에서는 별로 사용되지 않을 거 같으나 기능구현 완료 후 코드 리팩토링에 자주 사용될 것으로 보인다. useReducer()는 특정 기능에 필요한 상태들을 모두 객체 안에 포함시켜 값을 전체적으로 변경하고 갱신하는 로직에 자주 사용할 것 같다.