6. useCallback

useMemo와 같이 메모이제이션 기법 중 하나이며 컴포넌트의 성능을 최적화하기 위한 도구 중 하나입니다.

useCallback은 함수의 반환 값을 메모이제이션 하는 것(useMemo)이 아니라 함수 자체를 캐싱해주는 Hook입니다.

 

함수형 컴포넌트는 함수이며 이러한 함수형 컴포넌트를 렌더링 한다는 것은 컴포넌트 함수를 호출하는 것입니다. 그렇기 때문에 내부 변수들은 렌더링이 될때마다 초기화됩니다. 이 과정에서 useCallback으로 원하는 함수를 묶어서 렌더링이 다시 되어도 초기화 되는 것을 막을 수 있습니다. 여기서 주의할 점은 useCallback으로 함수를 메모이제이션 할려면 해당 함수를 작성할 때 함수 표현식으로 작성해야 한다는 것 입니다. 만약에 함수 선언식으로 함수를 작성한다면 호이스팅되기 때문에 메모이제이션 할 수 없습니다.

 

useCallback의 구조 

useCallback(() => {
	// 메모이제이션 해 줄 콜백함수
},[의존성 배열])

// 사용 예

const functionName = useCallback((num) => {
	return num + ;1
}, [state])

useMemo와 동일하게 의존성 배열 내부에 있는 값이 변경되지 않는 이상 다시 초기화 되지 않습니다.

 

예제를 사용해보며 이해해 봅시다.

import {useState} from 'react';


export default function App () {

	const [val, setVal] = useState(100)
    const [isDark, setIsDark] = useState(false)
    
    const boxstyle = () => {
    	return (
        	backgroundColor: 'pink',
            width: `${val}px`,
            height: `${val}px`,
        )
    }



	return (
    	<div style={{
        	background : isDark ? "black" : "white"
        }}>
    		<input 
            	type = 'number'
                value = {val}
                onChange = { (e) => {setVal(e.target.value)} }
            />
            <button onClick={setIsDark(!isDark)}>
            	{isDark ? 다크 모드 : 화이트 모드}
            </button>
            <Box createBoxStyle={boxstyle} />
    	</div>
    )
}
import {useEffect, useState} = form 'react'

export default const Box({ createBoxStyle }) {
	const [style, setStyle] = useState({});
    
    useEffect(()=>{
    	console.log("박스 크기");
        setStyle(createBoxStyle());
    },[createBoxStyle]);
    
    return <div style={style} />
}

 

위 코드 같은 경우 state가 바뀌면서 재 렌더링 되는 경우 boxstyle 함수가 초기화 되기 때문에 isDark가 변경되어도 "박스 크기"가 콘솔에 뜨게 됩니다. 박스 크기와 직접적인 연관이 없는 isDark가 변경딜 때는 Box 컴포넌트에 있는 useEffect를 실행시키지 않는 것이 효율적이기 때문에 useCallback을 이용해 boxstyle 함수를 묶어주는 것이 좋습니다.

 

묶어준 코드는 다음과 같습니다.

import {useState, useCallback} from 'react';


export default function App () {

	const [val, setVal] = useState(100)
    const [isDark, setIsDark] = useState(false)
    
    const boxstyle = useCallback(() => {
    	return (
        	backgroundColor: 'pink',
            width: `${val}px`,
            height: `${val}px`,
        )
    },[val])



	return (
    	<div style={{
        	background : isDark ? "black" : "white"
        }}>
    		<input 
            	type = 'number'
                value = {val}
                onChange = { (e) => {setVal(e.target.value)} }
            />
            <button onClick={setIsDark(!isDark)}>
            	{isDark ? 다크 모드 : 화이트 모드}
            </button>
            <Box createBoxStyle={boxstyle} />
    	</div>
    )
}

이렇게 작성하면 박스 크기를 지정하는 val state가 변경될때만 boxstyle 함수가 초기화 되기 때문에 불필요하게 Box 컴포넌트의 useEffect를 호출하는 상황을 해결할 수 있습니다.

사실 위의 useCallbak 내부의 콜백 함수는 값만 반환해 주기 때문에 useMemo를 사용해도 문제 없습니다.

 



7. useReducer

리액트에서 state 관리를 위한 또 다른 Hook입니다. 보통 여러개의 하위값을 가지고 있는 복잡한 state를 다루는 경우 useReducer를 사용하면 코드를 매우 깔끔하게 짤 수 있습니다.

 

이러한 useReducer는 Reducer, Dispatch, Action으로 이루어져 있습니다.

간단하게 설명하자면 state를 변경해 주는 작업을 하는 것이 Reducer이고, 값의 변경을 요구하는 행위가 Dispatch, 요구하는 내용이 Action입니다.

Dispatch( Action )  ====>  Reducer( State, Action ) =====> state에 업데이트

 

예제를 보면서 이해해 봅시다.

 

(예제 1 : 은행)

import {useState, useReducer} from 'react';

//reducer : state의 값을 변경해줌
//dispatch : state를 업데이트 하기 위한 요구
//actioin : 요구의 내용, 보통 객체의 형태로 보냅니다. 

const reducer = (state, action) = > {
	switch (action.type) {
    	case 'deposit':
        	return state + action.payload;
        case 'withdraw':
        	return state + action.payload;
        default:
        	return state;
    }
};

const ACTION_TYPE = {
	deposit : 'deposit',
    withdraw : 'withdraw',
}

export default fucntion App () {
	
    const [val, setVal] = useState(0)
    const [money, dispatch] = useReducer(reducer, 0) //(리듀서, 초기값)
    
    
    
	return (
    <div>
    	<h2>은행</h2>
        <p>잔고 : {money}원</p>
        <input 
        	type = 'number'
            value = {val}
            onChange = {(e) => { setVal(parseInt(e.target.value)) ]}
        	step = 1000
        />
        <button onClick={() => {
        	dispatch({
            	type: ACTION_TYPE.deposit,
                payload : number,
            })
        }}>출금</button>
        <button onClick={
        	dispatch({
            	type: ACTION_TYPE.withdraw,
                payload : number,
            })
        }>예금</button>
    </div>
    )
}

위의 코드 처럼 state를 업데이트 해줄 reducer를 설정해 주고, dispatch로 원하는 작업을 설정할 수 있습니다. 사실 위와 같은 상황에서 money state는 useState를 사용해도 될 정도로 간단한 작업을 수행합니다. 다음 예제를 통해 복잡한 작업에서 빛을 발하는 useReducer를 확인해 봅시다.

 

(예제 2 : 출석부)

import {useState, useReducer} from 'react';

const ACTYPE = {
	add : 'add',
    delete : 'delete',
    hereCheck : 'hereCheck',
    
}

const initial = {
	count: 0,
    students : [],
}

const reducer = (state, action) => {
	switch(action.type) {
    	case ACTYPE.add:
            return {
            	count: state.count + 1,
                students: [...state.students, action,addInfo],
            };
        
        case ACTYPE.delete:
            return {
            	count:state.count - 1
                students: state.students.filter((infoOne) => infoOne.id !== action.deleteId),
            };
            
        case ACTYPE.hereCheck:
        	return {
            	count : state.count,
                students : state.students.map((student)=>{
                	if(action.hereId === student.id) {
                    	{
                        	...student, 
                        	isHere: !student.isHere	
                        }
                    };
                    return student; 
                }) 
            }
        
        default:
        	return state;
    }
};



export default function App () {
	
    const [name, setName] = useState("");
    const [nameBook, dispatch] = useReducer(reducer, initial);
    
    const handleAdd = () => {
    	dispatch({
        	type: ACTYPE.add,
        	addInfo: {
            	id: Date.now(),
                name : name,
                isHere : false
            },
        })
        setName("")
    }
    
    const handleDelete = (id) => {
    	dispatch({
        	type : ACTYPE.delete,
        	deleteId : id,
        })
    }
    
    const handleHere = (id) => {
    	dispatch({
        	type: ACTYPE.hereCheck,
            hereId : id,
        })
    }
    
    
	return (
    <div>
    	<h2> 출석부 출석부~ </h2>
        <p> 총 학생 수 : {nameBook.count} </p>
        <input
        	type = 'text'
            value = {name}
            onChange = { (e) => { setName(e.target.value) } }
        />
        <button onClick={handleAdd}> 추가 </button>
        {
        	nameBook && 
         	<ul>
        		{
            		nameBook.students.map((student) => {
            			return ( 
                        	<li key={student.id}>
                                <span 
                                	onClick = {() => handleHere(student.id)}
                                    style = {
                                        student.isHere 
                                        ? {color:gray, textDecoration: line-through}
                                        : {}
                                    }
                                }>
                                    {student.name}
                                </span>
                            	<button onClick={() => handleDelete(student.id)}>삭제</button>
                        	</li>
                        )
            		})
            	}
        	</ul>   
        }
    </div>
    )
}

위 코드를 보면 state에 대한 복잡한 작업을 reducer에서 수행해 주는 것을 볼 수 있습니다. 앞으로 위와 같이 하위 값들이 있는 state에 대한 복잡한 작업을 해야하는 경우 useReducer를 사용하여 가독성 높은 코드를 작성해 보면 좋을 것 같습니다.

 

4. useContext

리액트로 만든 어플리케이션은 여러개의 컴포넌트들로 이루어져 있습니다. 최상위 App 컴포넌트에서 아래로 뻗어나가는 트리 형태로 이루어져 있고, 부모 컴포넌트에서 자식 컴포넌트로 Props가 전달이 되는 구조입니다.

 

만약 컴포넌트가 매우 많이 존재하는 상태에서 특정 Props를 전달해야하는 상황이라면 전달하는 과정이 매우 복잡해 질 것이고 Props를 수정해야 한다면 복잡도는 더욱 올라갈 것입니다.

 

리액트에서는 이러한 문제점을 간편하게 해결해 줄 수 있는 Context API를 제공해 주고 있습니다. 이러한 Context API 전역적으로 사용해야 하는 데이터를 여러 컴포넌트에서 편리하게 가져와 사용할 수 있는 방법을 제공합니다.

 

context를 이용해 전역적으로 공유할 데이터를 지정하고 각 컴포넌트에서는 useContext로 해당 context를 가져와 사용할 수 있습니다. 이러한 context는 꼭 필요한 경우에만 사용해야 합니다.

 

context를 이용해 데이터를 전역적으로 사용할 수 있도록 하는 방법을 알아봅시다.

 

1. 먼저 context를 만들어줍니다.

import {createContext} from 'react';

interface MyContextType = {
	name: string;
    age: number;
    isAdult: boolean;
    setAge: Dispatch<SetStateAction<number | undefined>>;
}

const MyCOntext = createContext<MyContextType>(
	// 데이터 예시 
    // js라면 그냥 null을 넣고 
    // 원하는 데이터를 Provider를 통해 뿌려줄 때 value에 작성하면 됩니다.
    {
    	name : null,
        age : null,
        isAdult : null,
        setAge: () => {},
    }
);

 

2. 만들어준 context의 데이터를 최상위 컴포넌트에서 Provider로 뿌려줍니다.

import {MyContext} from './context/MyContext';
import Component1 from './component/Component1';

export default function App () {
	return (
    <MyContext.Provider value={{name = '이름', age = 23, isAdult = true, setAge}}>
    	<Component1/>
    </MyContext.Provider>
    )
}

 

3. context 데이터를 사용해야 하는 컴포넌트에서 useContext로 받아서 사용하면 됩니다.

import {useContext} from 'react'
import {MyContext} from './context/MyContext';

export default function ChildComponent () {
	
    const {name, age, isAdult, setAge} = useContext(MyContext);
    
	return (
    <div>
    	<p> name : {name} <p>
        {isAdult && 
        	<p> age : {age} </p>
        }
    </div>
    )
}

 



 

 

5. useMemo

참고한 강의 :

 

useMemo에서 Memo는 Memoization을 뜻합니다. 이는 동일한 값을 리턴하는 함수를 반복적으로 호출해야 한다면, 맨 처음 계산한 값을 메모리에 저장하고, 필요할 때마다 호출하는 것이 아니라 저장한 값을 꺼내서 재사용하는 것을 말합니다. 

 

useMemo의 구조

const value = useMemo(()=>{
	//메모이제이션 할 값을 계산해서 반환해 주는 함수
    return 함수();
},[해당 요소의 값이 업데이트 될 때만 콜백 함수를 다시 호출하여 메모이제이션])

만약 의존성 배열이 빈 배열이라면 처음 렌더링 될 때의 값을 메모이제이션 하고 이후에는 메모이제이션 된 값을 가져와 사용합니다. 이런 useMemo도 무분별하고 사용한다면 오히려 성능을 저하시킬 수 있습니다. 메모이제이션 자체가 값을 재활용하기 위해 따로 메모리를 소비하는 것이기 때문에 불필요한 값들까지 캐시해 버린다면 성능이 저하될 것입니다. 그렇기에 꼭 필요할 때만 사용해야 합니다.

 

예제를 통해 이해해보면 좋을 것 같습니다.

import {useState} from 'react'

const hardCal = (number) => {
	console.log("어려운 계산");
    for (let i = 0; i < 999999999; i++) {}
    return number + 10000;
}

export default function App(){
	
    const [hnumber, setHnumber] = useState(1);
    
    const hSum = hardCal(hnumber);

	return (
    	<div>
    		<h3> 어려운 계산 </h3>
            <input 
            	type = 'number'
                value = {hnumber}
                onChange = {(e) => {setHnumber(parseInt(e.target.value))}}
            />
            <span> +10000 = {hSum} </span>
        </div>
    )
}

위와 같은 코드의 경우 state에 10000을 더하는 시간이 엄청 오래 걸리기 떄문에 렌더링 할때마다 계산하는 함수를 호출하는 것은 매우 비효율적입니다. 이 경우 저 hardCal 함수로 변수를 초기화 하는 코드에 useMemo를 이용하면 됩니다. 이럴 경우 특정 조건에만 함수를 다시 호출하기 때문에 불필요하게 시간이 오래걸리는 어려운 계산을 하지 않습니다.

useMemo를 적용한다면 다음 코드와 같습니다. useMemo를 적용할 경우 효과적인 상황을 연출하기 위해 쉬운 계산 함수를 사용하는 코드도 함께 작성했습니다.(강의 참고)

import {useState, useMemo} from 'react'

const hardCal = (number) => {
	console.log("어려운 계산");
    for (let i = 0; i < 999999999; i++) {}
    return number + 10000;
}

const eazyCal = (number) => {
	console.log("쉬운 계산");
    return number + 10000;
}

export default function App(){
	
    const [hnumber, setHnumber] = useState(1);
    const [enumber, setEnumber] = useState(1);
    
    //const hSum = hardCal(hnumber);
    const hSum = useMemo(()=>{
    	return hardCal(hnumber);
    },[hnumber]);
    const eSum = eazyCal(enumber);

	return (
    	<div>
    		<article>
                <h3> 어려운 계산 </h3>
                <input 
                    type = 'number'
                    value = {hnumber}
                    onChange = {(e) => {setHnumber(parseInt(e.target.value))}}
                />
                <span> +10000 = {hSum} </span>
            <article>
            
            <article>
                <h3> 쉬운 계산 </h3>
                <input 
                    type = 'number'
                    value = {enumber}
                    onChange = {(e) => {setEnumber(parseInt(e.target.value))}}
                />
                <span> +10000 = {eSum} </span>
            <article>
        </div>
    )
}

위처럼 hCal에 대한 반환 값을 useMemo를 이용해 캐싱해 준다면, hnumber가 업데이트 되지 않는한 재 랜더링이 일어날 때, 캐시한 반환 값을 보내주기 때문에 기존 렌더링 할때마다 어려운 계산을 하던 문제를 해결할 수 있습니다. 즉 쉬운 계산의 숫자를 바꾸면 쉬운 계산 함수만 실행되는 것이고, 어려운 계산의 숫자를 바꿔줄 때만 어려운 계산 함수를 호출하며 효율적인 코드를 작성할 수 있습니다.

 

하지만 위와 같은 상황이 실제 개발 환경에서 일어날 경우는 많지 않습니다. 실제로 useMemo를 유용하게 사용하는 상황을 알아봅시다.

import {useState, useEffect} from 'react';


export default function App () {
	const [num, setNum] = useState(0);
    const [isKorea, setIsKorea] = useState(true);
    
    const location = isKorea ? "한국" : "외국"
    
    useEffect(()=>{
    	console.log("useEffect 호출");
    },[location])

	return (
    	<div>
    		<section>
            	<h2>하루에 몇끼 먹음?</h2>
                <input 
                	type = 'number'
                    value = {num}
                    onChange = {(e) => { setNum(e.target.value)} }
                />
            </section>
            
            <section>
            	<h2>어느 나라에 있음?</h2>
                <p> 나라 : {location} </p>
                <button onClick={ () => {setIsKorea(!isKorea)} }> 비행기 탐 </button>
            </section>
        </div>
    )

}

위의 코드의 경우에는 location이 변경 되었을 때만 useEffect가 잘 호출됩니다. 하지만 여기서 location을 객체로 만든다면 어떻게 될까요?? location 값 변경에 관여하는 isKorea가 아닌 num이 변경되도 useEffect가 실행됩니다. 그 이유는 객체 타입은 내부 내용을 메모리 공간에 넣고 해당 메모리에 대한 주소를 변수에 할당되기 때문입니다.

 

여기서 원시 타입과 객체 타입의 차이를 설명드리자면 

원시 타입

  • String
  • Number
  • Boolean
  • Null
  • Undefined
  • Bight
  • Symbol

을 제외한 모든 타입이 객체 타입인데 대표적으로 Object와 Array가 있습니다. 그리고 원시 타입이 변수에 값을 초기화 할 경우 바로  값을 넣을 수 있는 공간을 만들어 저장하는 데, 객체 타입은 그 내부 내용이 너무 크기 때문에 위에서 설명했듯이 메모리 공간을 할당 받아 메모리에 값을 넣어주고 해당 메모리의 주소를 변수에 할당합니다. 이로 인해서 location에 객체를 넣어준다면, 값 변경에 직접적으로 관여하는 isKorea가 아닌 다른 state가 변경되면서 재 렌더링이 되더라도 location은 새로운 메모리 주소를 할당받기 때문에 useEffect는 location이 변경된 것으로 인지합니다.

 

이런 경우에 location을 useMemo에 넣어줌으로써 문제를 해결할 수 있습니다.

즉, location에 할당되는 주소 값을 isKorea가 변경되었을 때만 새로 메모리 주소 값을 할당 받고 변경된 값을 객체에 반영할 수 있게 하는 것입니다. 코드는 다음과 같습니다.

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


export default function App () {
	const [num, setNum] = useState(0);
    const [isKorea, setIsKorea] = useState(true);
    
    const location = useMemo(()=>{
    	return (
        	{
    			country: isKorea ? "한국" : "외국"
    		}
        )
    },[isKorea]) 
    
    useEffect(()=>{
    	console.log("useEffect 호출");
    },[location])

	return (
    	<div>
    		<section>
            	<h2>하루에 몇끼 먹음?</h2>
                <input 
                	type = 'number'
                    value = {num}
                    onChange = {(e) => { setNum(e.target.value)} }
                />
            </section>
            
            <section>
            	<h2>어느 나라에 있음?</h2>
                <p> 나라 : {location} </p>
                <button onClick={ () => {setIsKorea(!isKorea)} }> 비행기 탐 </button>
            </section>
        </div>
    )

}

 

다음 글에서는 useCallback과 useReducer를 정리해 보겠습니다.

최근 프로젝트 위주로 개발을 진행했고, 특정 기술만 주로 사용하다보니 시야가 좁아진다는 느낌을 많이 받았습니다. 그래서 이번 기회에 React Hooks부터 JS, HTML 태그들, CSS까지 전체적으로 복습해보며 시야를 넓히는 연습을 해볼려고 합니다. 이 과정을 공유하여 개발을 공부하는 분들에게도 도움이 되었으면 하는 마음에 이렇게 블로그에 남기게 되었습니다!

 

처음 다루어 볼 녀석은 React의 Hook들입니다. 이 글을 읽고 있는 여러분들도 알고 계시겠지만 Hook이 나오게 된 배경을 아주 간단하게 이야기 하고 바로 본론으로 넘어가겠습니다. 지금부터 편하게 리액트와 훅이라고 부르겠습니당 하핳

 

리액트는 처음에 컴포넌트를 상속 받는 클래스형으로 컴포넌트로 만들었고, state와 리액트의 라이프 사이클을 이용했습니다. 하지만 매번 컴포넌트를 상속 받아서 컴포넌트를 만들어야 하는 방식은 불편했고, 이에 대한 해결책으로 나온 것이 바로 함수형 컴포넌트입니다. 여기서 문제는 함수형 컴포넌트는 기존 클래스형에서 사용하던 state와 리액트의 라이프사이클을 이용하기에는 어려움이 있었다는 것입니다. 이러한 문제를 해결해 준 주인공이 바로 Hook들입니다. 대표적으로 state를 함수형 컴포넌트에서 사용할 수 있는 useState와 리액트 라이프사이클을 편리하게 이용할 수 있는 useEffect 등이 있습니다.

 

이제 본격적으로 리액트 훅들에 대해 공부해보도록 하겠습니다.

그 전에 주의할 점 짚고 시작하겠습니다.

  • JSX 구문이 컴포넌트를 구분할 수 있도록 컴포넌트 이름의 앞 첫글자는 무조건 대문자로 작성해야한다.
  • 컴포넌트에서 return 구문 아래에는 하나의 태그만 존재해야한다.

 

1. useState

먼저 state란 컴포넌트가 가질 수 있는 상태를 말합니다. useState는 이러한 state를 간편하게 생성하고 값을 설정할 수 있도록 해줍니다. 

import {useState} from 'react'

export default function MyComponent () {
	
    // js
    const [state, setState] = useState(초기값);
    
    // ts
    const [state, setState] = useStatr<타입 지정>(초기값);
    // number, string, boolean, number[], string[] 등등
    
	return <></>
}

위와 같이 import를 해주고 초기값과 값을 가지고 있는 state, 값을 설정할 수 있는 setState를 지정해줍니다.

그리고 여기서 중요한 점은 state 값을 변경할 경우 해당 컴포넌트는 재렌더링 된다는 것입니다.

 

또한 state를 변경할 때는 무조건 setState를 사용해야 합니다. 만약에 state가 배열이나 객체일 경우에는 스프레드 연산자를 이용해 state를 복사한 후 값을 변경하고 복사한 녀석을 setState의 인자값으로 넣어주면 됩니다. 

import {useStat} from 'react'

export default function MyComponent () {
	
    const [stateArr, setStateArr] = useState<string[]>(["깁밥", "콩나물", "완두콩"]);
    const [state, setState] = useState<string>("");
	
    const deleteState = (state:string) => {
    	// state 값 복사
        let copy:string[] = [...state];
        
        // 삭제할려는 데이터가 아닌 요소만 가진 배열 만들기
        const delete:string[] = copy.filter((data)=>{
        	return data !== state;
        })
        
        setState(delete);
    }
	
    // 이전 값에 간단한 추가를 하는 경우에는 
    // setState 내부에서 콜백 함수를 사용하여 편리하게 작업할 수 있다.
    const updataData = () => {
    	setStateArr((prev)=>{
        	return [state, ...prev];
        })
    }
    
    
	return (
    <div>
    	<input type='text' value={state} onChange={(e)=>{setState(e.target.value)}}
 		<button onClick = {updateData}>업데이트</button>   	
    	
        {stateArr && stateArr.map((state, index)=>{
        	return <p key={index} onClinck={() => deleteState(state)}>{state}</p>
        })}
    </div>
    )
}

 

 

 

 

그리고 state에 들어가는 초기값이 어떤 무거운 작업을 시행한 후 반환되는 값이라면, 처음 렌더링 될때 한번만 작업을 시행하는 것이 좋습니다. 하지만 그냥 useState(무거운작업함수) 이렇게 넣어버리면 state가 변경될때마다 무거운 작업이 수행되기 때문에 콜백 함수를 사용하여 처음 렌더링 될때만 state의 초기값을 설정하는 함수를 작동시키는 것이 좋습니다.

import {useState} from 'react'

const havyWork = () => {
	console.log('무거운 작업');
    return []
}


export default function MyComponent () {
	
    const [state, setState] = useState(() => {
    	return havyWork
    });
    

	return (
    	<div>
        	<p> {state} </p>
    	</div>
    )
}

 

 



 

2. useEffect

리액트의 라이프 사이클을 이용할 수 있도록 도와주는 Hook입니다.

리액트의 라이프 사이클을 크게 3개로 나눌 수 있는데 Mount(처음 렌더링) > Update(재렌더링) > Unmount(화면에서 사라짐)입니다. useEffect를 통해 원하는 시점에 원하는 작업을 수행할 수 있습니다.

 

1. 처음 렌더링 될때

useEffect(()=>{
	//원하는 작업
},[])

 

2. 렌더링이 될때마다 실행

useEffect(()=>{
	//원하는 작업
})

 

3. 특정 state가 변경될 때

useState(()=>{
	//원하는 작업
},[특정 값])

 

여기서 특정 값을 작성하는 배열은 '의존성 배열'이라고 부릅니다. 이러한 useEffect에서의 clean up에 대해 한 번 알아봅시다. clean up은 useEffect를 통해 특정 작업을 한 후 해당 작업에 대한 정리 작업을 실행하는 것을 이야기합니다.

 

여기서 의존성 배열에 아무것도 존재하지 않는 다면, 컴포넌트가 마운트 됐을때 부수 작업이 실행되고, 언마운트될때 정리 작업이 실행됩니다.

import {useEffect} from 'reat';


export default function Timer () {
	
    useEffect(()=>{
    	// 부수 작업
        const timer = setInterval(()=>{
        	console.log("타이머 실행 중");
        }, 1000);
        
        // useEffect에서 return을 사용하면 해당 컴포넌트가 Unmount 됐을 때 실행이 됩니다.
        return () => {
        	// 정리 작업
            clearInterval(timer);
            console.log("타이버 종료")
        }
    },[]);
    
	return (
    	<div>
        	<span> 타이머 컴포넌트여 </span>
        </div>
    )
}

 

만약에 의존성 배열에 특정 값이 존재한다면, 해당 값이 처음 변경 되었을 때는 부수 작업을 실행하고, 다음 작업부터 기존 state에 대한 정리 작업을 진행한 후 부수 작업을 진행합니다.

 

그리고 브라우저 종속성이 있는 작업을 진행하는 경우 useEffect로 묶는데 그 이유는 리액트가 가상 DOM을 사용하기 때문에 직접적인 DOM 조작은 렌더링 이후에 이루어져야합니다. useEffect는 렌더링 후에 실행되기 때문에 브라우저 종속성이 있는 작업을 실행할 때 useEffect로 감싸는 것입니다.

 

useEffect의 실행 시점

  • 초기 렌더링 후: 컴포넌트가 처음 렌더링된 직후에 useEffect의 부수 효과 코드가 실행됩니다.
  • 리렌더링 후: 의존성 배열에 포함된 값이 변경되어 컴포넌트가 리렌더링될 때, 이전의 정리 함수가 실행된 후에 새로운 부수 효과 코드가 실행됩니다.
  • 언마운트 직전: 컴포넌트가 언마운트되기 직전에 useEffect의 정리 함수가 실행됩니다.

 



 

3. useRef

ref 객체를 반환해주는 Hook입니다. useRef를 이용해 설정하는 초기값은 ref 객체의 current에 저장됩니다.

이러한 ref는 크게 두 가지 용도로 사용됩니다.

 

1. 저장 공간

ref의 값을 변경해도 리렌더링 되지 않기 때문에  컴포넌트 내의 변수들의 값을 유지한 채 특정 값을 변경하고 저장하고 싶을 때 유용합니다. 그리고 컴포넌트가 렌더링 되어도 ref의 값은 유지됩니다.  즉, ref의 값은 컴포넌트가 마운트 된 이후 언마운트될 때까지 값이 유지되는 것입니다.

 

버튼을 누르면 값을 증가시켜 주는 예제를 통해 차이를 비교해보겠습니다.

import {useState, useRef} from 'react';

export default function CountComponent () {
	
    const [state, setState] = useState<number>(0);
    const ref = useRef<number>(0)
    // 요소를 참조할 경우 const ref = useRef<HTML요소이름Element>(null);
    
    console.log("렌더링 ")
    
    return (
    <div>
    	<p> state: {state} </p>
        <p> ref : {ref} </p>
        <button onClick = {() => { setState( state + 1) }}> state 값 올리기 </button>
    	<button onClick = {() => { ref.current = ref.current + 1} }> ref 값 올리기 </button>
    </div>
    )

}

위의 코드를 실행하고 'state 값 올리기' 버튼을 올리면 "렌더링"이 콘솔 창에 버튼을 누를 때마다 뜨고 변경된 값이 바로바로 화면에 적용되어 보일 것입니다, 하지만 'ref 값 올리기'를 클리하면 렌더링이 일어나지 않기 때문에 ref의 값이 버튼을 클리할 때마다 1씩 증가하고 있지만 그 변화가 화면에 반영되어 나타나지는 않습니다. 이 때 'state 값 올리기' 버튼을 클릭할 경우 증가한 ref의 값도 반영되어 화면에 표시될 것입니다.

 

때문에 값이 자주 바뀌는 경우 ref를 사용하는 게 성능상 유리할 것입니다. 그리고 변화를 감지해야 하지만 변화를 감지할 때 렌더링을 발생시키지 않아야하는 경우에 사용하기 유용할 것입니다.

 

 

2. DOM 요소 접근

ref를 지정한 요소에 접근할 수 있고, ref.current.focus(), ref.current.value, ref.current.style.backgroundColor, ref.current.checked 등 기존 document.querySelector로 불러와서 할 수 있는 작업들을 동일하게 할 수 있습니다.


이런식으로 사용하기 위해선 태그의 ref 속성으로 useRef를 넣어주면 DOM 요소에 쉽게 접근할 수 있는 것입니다.

이러한 ref를 사용하는 다양한 예제를 보면서 이해하면 좋을 것 같습니다.

 

1. 렌더링 될 때, 특정 input 요소에 포커싱

import { useRef, useEffect } from 'react';

export default function FocusInput() {
  const inputRef = useRef(null);

  useEffect(() => {
    inputRef.current.focus(); // input 요소에 포커스를 설정
  }, []);

  return <input ref={inputRef} type="text" />;
}

 

 

2. DOM 요소의 값을 읽고 설정

import { useRef } from 'react';

// input의 value 가지고 와서 설정하기
export default function ReadWriteInput() {
  const inputRef = useRef(null);

  const handleClick = () => {
    alert(inputRef.current.value); // input 요소의 현재 값을 읽음
    inputRef.current.value = 'Hello, World!'; // input 요소의 값을 설정
  };

  return (
    <div>
      <input ref={inputRef} type="text" />
      <button onClick={handleClick}>Show and Set Value</button>
    </div>
  );
}


// input이 체크되어 있는지 확인하고 변경하기
export default function RadioComponent() {
  const radioRef = useRef(null);

  const handleClick = () => {
    // 현재 체크 상태를 읽음
    console.log('Radio button is checked:', radioRef.current.checked);

    // 체크 상태를 설정
    radioRef.current.checked = !radioRef.current.checked;
  };

  return (
    <div>
      <input ref={radioRef} type="radio" name="example" />
      <button onClick={handleClick}>Toggle Radio Button</button>
    </div>
  );
}

 

 

3. DOM 요소 스타일 변경

import { useRef } from 'react';

export default function ChangeStyle() {
  const divRef = useRef(null);

  const handleClick = () => {
    divRef.current.style.backgroundColor = 'yellow'; // div 요소의 배경색 변경
  };

  return (
    <div>
      <div ref={divRef} style={{ width: '100px', height: '100px', backgroundColor: 'blue' }}></div>
      <button onClick={handleClick}>Change Color</button>
    </div>
  );
}

 

 

4. 스크롤 조작

import React, { useRef } from 'react';

export default function ScrollBox() {
  const boxRef = useRef(null);

  const handleScrollToBottom = () => {
    boxRef.current.scrollTop = boxRef.current.scrollHeight; // 스크롤을 가장 아래로 이동
  };

  return (
    <div>
      <div
        ref={boxRef}
        style={{ width: '200px', height: '100px', overflow: 'auto', border: '1px solid black' }}
      >
        <p>Some content here...</p>
        <p>More content here...</p>
        <p>Even more content...</p>
        <p>And more...</p>
        <p>More and more...</p>
        <p>Last content...</p>
      </div>
      <button onClick={handleScrollToBottom}>Scroll to Bottom</button>
    </div>
  );
}

 

 

5. 타미어 및 애니메이션 제어

import React, { useRef, useEffect } from 'react';

export default function TimerComponent() {
  const timerIdRef = useRef(null);

  useEffect(() => {
    // setInterval을 사용하여 1초마다 로그를 찍는 타이머 설정
    timerIdRef.current = setInterval(() => {
      console.log('Timer tick');
    }, 1000);

    return () => {
      // 컴포넌트 언마운트 시 타이머 제거
      clearInterval(timerIdRef.current);
    };
  }, []);

  return <div>Timer is running. Check the console for ticks.</div>;
}

이렇게 하는 이유는 컴포넌트가 다시 렌더링되어도 해당 타이머 및 애니메이션이 유지될 수 있도록 하기 위함입니다.

 

위와 같이 useRef를 이용해 다양한 작업들을 할 수 있습니다. 해당 글을 여기서 마무리하도록 하고 다음 글에 이어서 useContext, useMemo,  useCallback, useReducer에 대해 정리하겠습니다.

+ Recent posts