자바스크립트/러닝 리엑트

훅스 컴포넌트 개선하기

막이86 2023. 11. 13. 13:01
728x90

러닝 리엑트을 요약한 내용입니다.

  • 프롭, 상태가 바뀌면 컴포넌트 트리가 다시 렌더링되면서 최신 데이터를 반영한다.
  • useState를 사용해서 컴포넌트를 재렌터링을 하는 순간을 기술하는 주된 수단이였다.
  • 6장에서 살펴본 useState, useRef, useContext 이외에 원래부터 제공하고 있는 useEffect, useLayoutEffect, useReducer를 살표 보자
  • 컴포넌트 성능 최적화를 위한 useCallback과 useMemo를 살펴보자

7.1 useEffect 소개

  • 렌더링이 끝난 다음에 무언가를 하고 싶으면 어떻게 해야할까?

Checkbox를 체크하거나 해제할때 alert를 띄워주는 예제

  • App.js 컴포넌트 코드
  • import './App.css'; import CheckBox from './components/CheckBoxHooks'; function App() { return ( <div className="App"> <CheckBox></CheckBox> </div> ); } export default App;
  1. useState를 사용해서 checked의 값을 변경해주는 예제
    • alert를 클릭하지 않으면 렌더링이 되지 않는 문제가 있다.
      • alert이 블러킹 함수이기 때문
  2. import React, { useState } from 'react' export default function CheckBox() { const [checked, setChecked] = useState(false) alert(`checked: ${checked.toString()}`) return ( <> <input type="checkbox" value={checked} onChange={() => setChecked(checked => !checked)} /> {checked ? "checked" : "not checked"} </> ) }
  3. alert을 return 다음에 위치하는 예제
    • 말이 안되는 코드
  4. import React, { useState } from 'react' export default function CheckBox() { const [checked, setChecked] = useState(false) return ( <> <input type="checkbox" value={checked} onChange={() => setChecked(checked => !checked)} /> {checked ? "checked" : "not checked"} </> ) alert(`checked: ${checked.toString()}`) }
  5. useEffect를 사용하는 예제
    • 렌더가 부수효과로 무언가를 수행하게 하고 싶을 떄 useEffect를 사용한다.</aside>
    • <aside> 💡 부수 효과: 컴포넌트가 렌더링 외에 다른 일을 수행해야 하는 일을 효과(Effect)라고 부른다.
    • alert, console.log, 브라우저나 네이티브 API와 상호작용은 렌더링에 속하지 않는다.
      • useEffect를 사용하면 렌더링이 끝나기를 기다렸다가 alert나 console.log 등에 값을 제공할 수 있다.
      useEffect(() => {
      	console.log(checked ? "Yes, checked" : "No, notchecked");
      })
      
      • checked 값을 검사해서 localStorage의 값을 설정 할 수도 있다.
      useEffect(() => {
      	localStorage.setItem("checkbox-value", checked);
      })
      
      • useEffect를 사용해 DOM에 추가된 특정 텍스트 입력에 초점을 맞출 수 있다.
      useEffect(() => {
      	txtInputRef.current.focus();
      })
      
    useEffect를 렌러딩이 끝난 다음에 발생하는 함수라고 생각하자. 렌더가 시작되면 컴포넌트 안에서 현재 상태 값에 접근할 수 있고, 이 값을 사용해서 다른 일을 할 수 있다. 그 후 렌더링이 다시 시작되면 모든 일이 처음부터 다시 발생한다. 새값이 전달되고, 새로 렌더링이 이뤄지고, 효과가 새로 적용된다.
  6. import React, { useState, useEffect } from 'react' export default function CheckBox() { const [checked, setChecked] = useState(false) useEffect(() => { alert(`checked: ${checked.toString()}`) }) return ( <> <input type="checkbox" value={checked} onChange={() => setChecked(checked => !checked)} /> {checked ? "checked" : "not checked"} </> ) }

7.1.1 의존 관계 배열

  • useEffect는 useState, useReducer등의 다른 상태가 있는 훅스와 함께 작동하도로 설계됐다.
  • useEffect는 렌더링이 끝난 다음에 호출 된다.

2가지의 서로 다른 상태 변수가 있는 곳에서 useEffect를 사용한 예제

import React, {useState, useEffect } from 'react'

function App() {
  const [val, set] = useState("")
  const [phrase, setPhrase] = useState("example phrase")

  const createPhrase = () => {
    setPhrase(val)
    set("")
  }

  useEffect(() => {
    console.log(`typing ${val}`)
  })

  useEffect(() => {
    console.log(`saved phrase: ${phrase}`)
  })

  return (
    <>
      <label>Favorite phrase: </label>
      <input
        value={val}
        placeholder={phrase}
        onChange={e => set(e.target.value)}
      />
      <button onClick={createPhrase}>send</button>
    </>
  );
}

export default App;
  • 입력 필드가 바뀔 때마다 val이 변경되면서 컴포넌트가 다시 렌더링된다.
  • Send 버튼을 클릭하면 텍스트 영역의 val이 ""로 재설정 된다. 이때도 다시 렌더링이 된다.
  • 예제 코드는 예상대로 동작하지만 useEffect 훅이 쓸데 없이 많이 호출된다.
App.js:13 typing 
App.js:17 saved phrase: example phrase
App.js:13 typing S
App.js:17 saved phrase: example phrase
App.js:13 typing Sh
App.js:17 saved phrase: example phrase
App.js:13 typing Shr
App.js:17 saved phrase: example phrase
App.js:13 typing Shre
App.js:17 saved phrase: example phrase
App.js:13 typing Shred
App.js:17 saved phrase: example phrase
App.js:13 typing 
App.js:17 saved phrase: Shred

useEffect 훅을 구체적인 데이터 변경과 연동 하는 예제

useEffect(() => {
	console.log(`typing ${val}`);
}, [val]);

useEffect(() => {
	console.log(`saved phrase: ${phrase}`);
}, [phrase]);
  • 의존 관계 배열을 Effect에 추가해서 호출 시점을 제어할 수 있다.
  • val 값이 바뀔 때만 호출이 되고, phrase 값이 변경될 떄만 호출 된다.
App.js:13 typing 
App.js:17 saved phrase: example phrase
App.js:13 typing S
App.js:13 typing Sh
App.js:13 typing Shr
App.js:13 typing Shre
App.js:13 typing Shred
App.js:13 typing 
App.js:17 saved phrase: Shred

의존 관계 배열을 사용해 여러 값을 검사할 수 있다.

useEffect(() => {
	console.log("either val or phrase has changed");
}, [val, phrase]);

초기 렌더링 직후 한번만 호출할 수 있다.

useEffect(() => {
	console.log("only once after initial render");
}, []);
  • 최초 렌더링시에만 호출되는 효관느 초기화에 아주 유용하게 쓰일수 있다.

컴포넌트가 트리에서 제거될 때 호출 된다.

useEffect(() => {
	welcomeChime.play();
	return () => goodbyeChime.play();	
}, []);
  • useEffect에 함수를 반환하면 컴포넌트가 제거 될때 호출 된다.
  • useEffect는 생성과 제거시에 호출되기 때문에 설정과 정리에 사용할 수 있다.

뉴스 피드를 구독 및 해제 하는 코드

import React, {useState, useEffect } from 'react'

export default function NewsFeed() {
    const [posts, setPosts] = useState([])
    const addPost = post => setPosts(allPosts => [post, ...allPosts])

    useEffect(() => {
        newsFeed.subscribe(addPost)
        welcomChime.play()
        return () => {
            newsFeed.unsubscribe(addPost)
            goodbyeChime.play()
        }
    }, [])
}
  • 랜더링시 뉴스 피드를 구독 할수 있다.
  • 컴포넌트가 제거될 때 뉴스 피드를 구독 취소 할 수 있다.

뉴스 피드를 구독 및 해제 하는 코드 관심사 분리

import React, {useState, useEffect } from 'react'

export default function NewsFeed() {
    const [posts, setPosts] = useState([])
    const addPost = post => setPosts(allPosts => [post, ...allPosts])

    useEffect(() => {
        newsFeed.subscribe(addPost)
        return () => {
            newsFeed.unsubscribe(addPost)
        }
    }, [])

    useEffect(() => {
        welcomChime.play()
        return () => {
            goodbyeChime.play()
        }
    }, [])
}
  • 기능을 여러 useEffect로 나눠 남는 것은 좋은 생각이다.

<aside> ❓ 호출 순서가 중요한 경우 우선 순위를 지정할 수 있을까???

</aside>

뉴스 피드를 구독 및 해제 하는 코드 커스텀 훅으로 구현

const useJazzyNews = () => {
    const [posts, setPosts] = useState([]);
    const addPost = post => setPosts(allPosts => [post, ...allPosts])

    useEffect(() => {
        newsFeed.subscribe(addPost)
        return () => {
            newsFeed.unsubscribe(addPost)
        }
    }, [])

    useEffect(() => {
        welcomChime.play()
        return () => {
            goodbyeChime.play()
        }
    }, [])

    return posts
}

function NewsFeed() {
    const posts = useJazzyNews()

    return (
        <>
            <h1>{posts.length} articles</h1>
            {posts.map(post => (
                <Post key={ post.id } {...pros} />
            ))}
        </>        
    )
}

7.1.2 의존 관계를 깊이 검사하기

  • 지금까지 배열에 추가한 의존관계는 문자열 뿐이다
  • 자바스크립트 기본 타입(문자열, 수)은 비교가 가능하다.
if ("gnar" === "gnar") {
	console.log("gnarly!!");
}
  • 객체, 배열, 함수 등을 비교하려면 비교 방법이 다른다.
    • 길이나 원소가 모두 같지만 두 배열 [1, 2, 3]과 [1, 2, 3]은 같지 않다.
    • 서로 다른 배열 인스턴스이기 때문에 배열의 값을 비교하려면 예상과 다른 결과를 얻음
if ([1, 2, 3] !== [1, 2, 3]) {
	console.log("but they are the same");
}
  • 배열에 값을 저장하는 변수를 만들고 비교하면 예상과 같은 결과를 얻을 수 있다.
    • 자바스크립트에서는 배열, 객체, 함수는 같은 인스턴스일 때만 서로 같다
const array = [1, 2, 3]
if (array === array) {
	console.log("but they are the same");
}

useEffect의 의존 관계 배열과 어떠한 관계가 있을까?

  1. 강제로 렌더링 할 수 있는 컴포넌트를 만들어 보자
    • 키보드가 눌릴 때마다 컴포넌트를 다시 랜더링하는 훅
    const useAnyKeyToRender = () => {
        const [, forcRender] = useState()
    
        useEffect(() => {
            window.addEventListener("keydown", forcRender)
            return () => window.removeEventListener("keydown", forcRender)
        }, [])
    }
    
  2. App 컴포넌트에서 강제로 렌더링 되는 훅을 사용해보자
  3. function App() { useAnyKeyToRender() useEffect(() => { console.log("fresh render") }) return <h1>Open the console</h1> }
  4. word 값을 참조하고, word가 변경 되면 App 컴포넌트를 다시 렌더링 하는 코드
    • word가 변경이 되지 않음으로 재렌더링은 이루어지지 않음
    • word가 변경되더라도 예상대로 동작함
    function App() {
        useAnyKeyToRender()
    
        const word = "gnar"
        useEffect(() => {
            console.log("fresh render")
        }, [word])
    
        return <h1>Open the console</h1>
    }
    
  5. 한 단어가 아닌 배열을 사용하면 어떠한 일이 벌어질까?
    • 렌더링이 이뤄질 때마다 새로운 배열이 선언되기 때문에 자바스크립트는 words가 변경이 되었다고 가정하고 매번 재렌더링을 하게 된다.
    • App 컴포넌트라 재렌더링 되면 words는 새로운 인스턴스가 생기기 때문에 발생하는 문제
    function App() {
        useAnyKeyToRender()
    
        const words = ["sick", "powder", "day"]
        useEffect(() => {
            console.log("fresh render")
        }, [word])
    
        return <h1>Open the console</h1>
    }
    
  6. 배열을 사용는 방법
    • words를 App영역 밖에서 정의하면 문제가 해결
      • 함수 밖에 선언된 words를 인스턴스를 가리킨다.
      • 재렌더링이 된다고 하더라도 words를 새로 인스턴스를 생성하지 않기 때문에 해결 가능
      <aside> 💡 이 방법은 별로 추천하지 않는 방법임
    • </aside>
    const words = ["sick", "powder", "day"]
    function App() {
        useAnyKeyToRender()
    
        useEffect(() => {
            console.log("fresh render")
        }, [word])
    
        return <h1>Open the console</h1>
    }
    
  7. 컴포넌트 안에 변수를 만들어야하는 경우
    import React from 'react'
    import WordCount from './components/WordCount';
    
    function App() {
      return (
        <>
          <WordCount>You are not going to believe this but...</WordCount>
        </>
      );
    }
    
    export default App;
    
    • WordCount 컴포넌트는 childern을 프로퍼티로 받아서 words를 배열로 만들었다.
    • words가 변경될 때마다 컴포넌트를 다시 랜더링 할 수 있다.
    • 하지만 키를 누르면 콘솔에 "fresh render"라는 단어가 출력된다.
  8. import React, { useState, useEffect } from 'react' const useAnyKeyToRender = () => { const [, forcRender] = useState() useEffect(() => { window.addEventListener("keydown", forcRender) return () => window.removeEventListener("keydown", forcRender) }, []) } export default function WordCount({ children = "" }) { useAnyKeyToRender() const words = children.split(" ") useEffect(() => { console.log("fresh render") }, [words]) return ( <> <p>{children}</p> <p> <strong>{words.length} - words</strong> </p> </> ) }
  9. 불필요한 추가적인 렌더링을 피할 수 있는 방법이 있다.
    • useMem는 메모화된 값을 계산하는 함수를 호출 한다.
    • 리엑트에서 useMemo를 사용하면 캐시된 값과 계산한 값을 비교해서 실제 값이 변경됐는지 검사해준다.
    • useMemo에 의존관계를 전달하지 않으면 렌더링이 될때마다 값을 재계산한다.
    • const words = useMemo(() => { const words = children.split(" ") return words }, [])
    • useEffect와 마찬가지로 useMemo에도 의존관계 배열을 추가 해야함
    • const words = useMemo(() => { const words = children.split(" ") return words }, [children])
    useMemo를 사용해서 words 배열은 children에 의존하고, children이 바뀌면 그에 맞춰 words의 값도 재계산이 된다.
  10. import React, { useState, useEffect, useMemo } from 'react' const useAnyKeyToRender = () => { const [, forcRender] = useState() useEffect(() => { window.addEventListener("keydown", forcRender) return () => window.removeEventListener("keydown", forcRender) }, []) } export default function WordCount({ children = "" }) { useAnyKeyToRender() const words = useMemo(() => { const words = children.split(" ") return words }, [children]) useEffect(() => { console.log("fresh render") }, [words]) return ( <> <p>{children}</p> <p> <strong>{words.length} - words</strong> </p> </> ) }

useCallback도 useMemo와 비슷하게 사용이 가능

  • useCallback은 값 대신 함수를 메모화 한다.
  • 예제 코드
    • [ ] 어떠한 내용을 이야기하는지 잘 모르겠음... ㅠㅠ
  • const fn = () => { console.log("hello") console.log("word") } useEffect(() => { console.log("fresh render") fn() }, [fn])

useJazzyNews 훅을 개선해보자

const useJazzyNews = () => {
    const [_posts, setPosts] = useState([])
    const addPost = post => setPosts(allPosts => [post, ...allPosts])
		const posts = useMemo(() => _posts, [_posts])

		useEffect(() => {
			newPostChime.play()
		}, [posts])

    useEffect(() => {
        newsFeed.subscribe(addPost)
        return () => {
            newsFeed.unsubscribe(addPost)
        }
    }, [])

    useEffect(() => {
        welcomChime.play()
        return () => {
            goodbyeChime.play()
        }
    }, [])

    return posts
}

function NewsFeed() {
    const posts = useJazzyNews()

    return (
        <>
            <h1>{posts.length} articles</h1>
            {posts.map(post => (
                <Post key={ post.id } {...pros} />
            ))}
        </>        
    )
}
728x90

'자바스크립트 > 러닝 리엑트' 카테고리의 다른 글

Suspense  (0) 2023.11.13
데이터 포함시키기  (0) 2023.11.13
리액트 상태 관리  (0) 2023.11.13
JSX를 사용하는 리액트  (1) 2023.11.13
리액트의 작동 원리  (0) 2023.11.13