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를 정리해 보겠습니다.

+ Recent posts