You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
component를 호출하여 렌더링을 통한 리액트 요소에는 호출 시점에 가지고 있던 props, state가 명시되어 있다.
모든 랜더링은 고유의 이벤트 핸들러를 가진다
비동기 함수안에 사용된 props, state는 호출 시점에 가지고 있던 props, state를 사용한다.
클로저 개념
모든 랜더링은 고유의 이펙트를 가진다
우리가 형성하고 있는 멘탈 모델 속에서 이펙트 함수는 이벤트 핸들러처럼 특정 랜더링에 속하는 함수라고 생각하면 된다.
리액트: state가 0 일 때의 UI를 보여줘.
컴포넌트:
여기 랜더링 결과물로 <p>You clicked 0 times</p> 가 있어.
그리고 모든 처리가 끝나고 이 이펙트를 실행하는 것을 잊지 마: () => { document.title = 'You clicked 0 times' }.
리액트: 좋아. UI를 업데이트 하겠어. 이봐 브라우저, 나 DOM에 뭘 좀 추가하려고 해.
브라우저: 좋아, 화면에 그려줄게.
리액트: 좋아 이제 컴포넌트 네가 준 이펙트를 실행할거야.
() => { document.title = 'You clicked 0 times' } 를 실행하는 중.
상태를 바꾼 후의 리액트 - 컴포넌트 - 브라우저와의 대화이다.
컴포넌트: 이봐 리액트, 내 상태를 1 로 변경해줘.
리액트: 상태가 1 일때의 UI를 줘.
컴포넌트:
여기 랜더링 결과물로 <p>You clicked 1 times</p> 가 있어.
그리고 모든 처리가 끝나고 이 이펙트를 실행하는 것을 잊지 마: () => { document.title = 'You clicked 1 times' }.
리액트: 좋아. UI를 업데이트 하겠어. 이봐 브라우저, 나 DOM에 뭘 좀 추가하려고 해.
브라우저: 좋아, 화면에 그려줄게.
리액트: 좋아 이제 컴포넌트 네가 준 이펙트를 실행할거야.
() => { document.title = 'You clicked 1 times' } 를 실행하는 중.
모든 랜더링은 고유의… 모든 것을 가지고 있다
클래스 컴포넌트로 만들면 이렇게 동작하지 않았습니다. 이 링크의 클래스 컴포넌트도 함수 컴포넌트와 같은 동작을 하리라고 착각하기 쉽다.
this.state.count 값은 특정 랜더링 시점의 값이 아니라 언제나 최신의 값을 가리킵니다. 그래서 매번 최종으로 업데이트가 되어있는 count로 찍혀있는 로그를 보게 됩니다.
흐름을 거슬러 올라가기
때때로 이펙트 안에 정의해둔 콜백에서 사전에 잡아둔 값을 쓰는 것이 아니라 최신의 값을 이용하고 싶을 때가 있다.
제일 쉬운 방법은 ref를 이용
functionExample(){const[count,setCount]=useState(0);constlatestCount=useRef(count);useEffect(()=>{// 변경 가능한 값을 최신으로 설정한다latestCount.current=count;setTimeout(()=>{// 변경 가능한 최신의 값을 읽어 들인다console.log(`You clicked ${latestCount.current} times`);},3000);});// ...
리액트로 어떠한 값을 직접 변경하는 것이 꺼림칙해 보입니다만, 리액트의 클래스 컴포넌트는 정확하게 이런 식으로 this.state 를 재할당하고 있습니다.
미리 잡아둔 props 및 state와는 달리 특정 콜백에서 latestCount.current 의 값을 읽어 들일 때 언제나 같은 값을 보장하지 않습니다. 정의된 바에 따라 이 값은 언제나 변경할 수 있습니다. 그렇기 때문에 이런 사용 방법은 기본 동작이 아니며, 여러분이 직접 가져다 써야 합니다.
이전 상태를 기준으로 상태 값을 업데이트 하고 싶을 때는, setState 에 함수 형태의 업데이터를 사용하면 된다.
이 이펙트가 한 번만 실행되었다 하더라도, 첫 번째 랜더링에 포함되는 인터벌 콜백은 인터벌이 실행될 때마다 c => c + 1 이라는 업데이트 지침을 전달하는데 완벽하게 들어맞습니다. 더 이상 현재의 count 상태를 알고 있을 필요가 없습니다. 리액트가 이미 알고 있으니까요.
함수형 업데이트와 구글 닥스(Google Docs)
오로지 필요한 최소한의 정보를 이펙트 안에서 컴포넌트로 전달하는게 최적화에 도움이 됩니다.
(결과보다) 의도를 인코딩하는 것은 구글 닥스가 협동 편집 문제를 해결한 방법과 유사합니다.
하지만 setCount(c => c + 1) 조차도 그리 좋은 방법은 아닙니다.
예를 들어 서로에게 의존하는 두 상태 값이 있거나 prop 기반으로 다음 상태를 계산할 필요가 있을 때는 도움이 되지 않습니다. 다행히도 setCount(c => c + 1) 은 더 강력한 자매 패턴이 있습니다. 바로 useReducer 입니다.
이 예제의 현재 동작은 step 이 변경되면 인터벌을 다시 시작하는 것입니다. 왜냐면 의존성으로 정의되어 있으니까요. 그리고 많은 경우에 이게 정확히 여러분이 원하던 동작일 겁니다! 이펙트를 분해하고 새로 설정하는데는 아무 문제가 없고, 특별히 좋은 이유가 있지 않다면 분해하는 것을 피하지 말아야 합니다.
하지만 step 이 바뀐다고 인터벌 시계가 초기화되지 않는 것을 원한다면?
이펙트의 의존성 배열에서 step 을 제거하려면 어떻게 해야할까?
어떤 상태 변수가 다른 상태 변수의 현재 값에 연관되도록 설정하려고 한다면, 두 상태 변수 모두 useReducer 로 교체해야 할 수 있습니다.
리듀서는 컴포넌트에서 일어나는 “액션”의 표현과 그 반응으로 상태가 어떻게 업데이트되어야 할지를 분리합니다.
이 경우조차 랜더링간 dispatch 의 동일성은 여전히 보장됩니다. 그래서 원한다면 이펙트의 의존성 배열에서 빼버릴 수도 있습니다. 이펙트가 재실행되도록 만들지 않을테니까요.
어떻게 다른 랜더링에 포함된 이펙트 안에서 호출된 리듀서가 props를 “알고” 있지?”
A: dispatch 를 할 때에 있다.
리액트는 그저 액션을 기억해 놓는다. 하지만 다음 랜더링 중에 리듀서를 호출할 것이다.
이 시점에서 새 props가 스코프 안으로 들어오고 이펙트 내부와는 상관이 없게된다.
이래서 제가 useReducer 를 Hooks의 “치트 모드” 라고 생각하는 것입니다. 업데이트 로직과 그로 인해 무엇이 일어나는지 서술하는 것을 분리할 수 있도록 만들어줍니다. 그 다음은 이펙트의 불필요한 의존성을 제거하여 필요할 때보다 더 자주 실행되는 것을 피할 수 있도록 도와줍니다.
함수를 이펙트 안으로 옮기기
흔한 실수 중 하나가 함수는 의존성에 포함 안해도 된다는 생각이다.
A common mistake is to think functions shouldn’t be dependencies.
functionSearchResults(){const[query,setQuery]=useState('react');// 이 함수가 길다고 상상해 봅시다functiongetFetchUrl(){return'https://hn.algolia.com/api/v1/search?query='+query;}// 이 함수가 길다고 상상해 봅시다asyncfunctionfetchData(){constresult=awaitaxios(getFetchUrl());setData(result.data);}useEffect(()=>{fetchData();},[]);// ...}
만약 이런 함수를 사용하는 어떠한 이펙트에도 deps를 업데이트하는 것을 깜빡했다면(아마도 다른 함수를 통해서요!), 이펙트는 prop과 state의 변화에 동기화하는데 실패할 것입니다. 그다지 좋지 않네요.
어떠한 함수를 이펙트 안에서만 쓴다면, 그 함수를 직접 이펙트 안으로 옮긴다.
functionSearchResults(){// ...useEffect(()=>{// 아까의 함수들을 안으로 옮겼어요!functiongetFetchUrl(){return'https://hn.algolia.com/api/v1/search?query=react';}asyncfunctionfetchData(){constresult=awaitaxios(getFetchUrl());setData(result.data);}fetchData();},[]);// ✅ Deps는 OK// ...}
useEffect 의 디자인은 사용자가 제품을 사용하다 겪을 때까지 무시하는 대신, 데이터 흐름의 변화를 알아차리고 이펙트가 어떻게 동기화해야할지 선택하도록 강제합니다.
하지만 저는 이 함수를 이펙트 안에 넣을 수 없어요
먼저, 함수가 컴포넌트 스코프 안의 어떠한 것도 사용하지 않는다면, 컴포넌트 외부로 끌어올려두고 이펙트 안에서 자유롭게 사용하면 됩니다.
// ✅ 데이터 흐름에 영향을 받지 않는다functiongetFetchUrl(query){return'https://hn.algolia.com/api/v1/search?query='+query;}functionSearchResults(){useEffect(()=>{consturl=getFetchUrl('react');// ... 데이터를 불러와서 무언가를 한다 ...},[]);// ✅ Deps는 OKuseEffect(()=>{consturl=getFetchUrl('redux');// ... 데이터를 불러와서 무언가를 한다 ...},[]);// ✅ Deps는 OK// ...}
혹은 useCallback 훅으로 감쌀 수 있습니다.
functionSearchResults(){// ✅ 여기 정의된 deps가 같다면 항등성을 유지한다constgetFetchUrl=useCallback((query)=>{return'https://hn.algolia.com/api/v1/search?query='+query;},[]);// ✅ 콜백의 deps는 OKuseEffect(()=>{consturl=getFetchUrl('react');// ... 데이터를 불러와서 무언가를 한다 ...},[getFetchUrl]);// ✅ 이펙트의 deps는 OKuseEffect(()=>{consturl=getFetchUrl('redux');// ... 데이터를 불러와서 무언가를 한다 ...},[getFetchUrl]);// ✅ 이펙트의 deps는 OK// ...}
입력을 받는 부분을 추가하여 임의의 query 를 검색할 수 있다면, query 를 인자로 받는 대신, getFetchUrl 이 지역 상태로부터 이를 읽어들인다.
functionSearchResults(){const[query,setQuery]=useState('react');constgetFetchUrl=useCallback(()=>{// No query argumentreturn'https://hn.algolia.com/api/v1/search?query='+query;},[]);// 🔴 빠진 의존성: query// ...}
functionSearchResults(){const[query,setQuery]=useState('react');// ✅ query가 바뀔 때까지 항등성을 유지한다constgetFetchUrl=useCallback(()=>{return'https://hn.algolia.com/api/v1/search?query='+query;},[query]);// ✅ 콜백 deps는 OKuseEffect(()=>{consturl=getFetchUrl();// ... 데이터를 불러와서 무언가를 한다 ...},[getFetchUrl]);// ✅ 이펙트의 deps는 OK// ...}
이것은 그저 데이터 흐름과 동기화에 대한 개념을 받아들인 결과입니다. 부모로부터 함수 prop을 내려보내는 것 또한 같은 해결책이 적용됩니다.
functionParent(){const[query,setQuery]=useState('react');// ✅ query가 바뀔 때까지 항등성을 유지한다constfetchData=useCallback(()=>{consturl='https://hn.algolia.com/api/v1/search?query='+query;// ... 데이터를 불러와서 리턴한다 ...},[query]);// ✅ 콜백 deps는 OKreturn<ChildfetchData={fetchData}/>}functionChild({ fetchData }){let[data,setData]=useState(null);useEffect(()=>{fetchData().then(setData);},[fetchData]);// ✅ 이펙트 deps는 OK// ...}
fetchData 는 오로지 Parent 의 query 상태가 바뀔 때만 변하기 때문에, Child 컴포넌트는 앱에 꼭 필요할 때가 아니라면 데이터를 다시 페칭하지 않는다.
함수도 데이터 흐름의 일부인가?
class lifecycle
classChildextendsComponent{state={data: null};componentDidMount(){this.props.fetchData();}componentDidUpdate(prevProps){// 🔴 이 조건문은 절대 참이 될 수 없다if(this.props.fetchData!==prevProps.fetchData){this.props.fetchData();}}render(){// ...}}
하지만 이렇게 하면 query 가 바뀌지 않았는데도 this.props.fetchData !== prevProps.fetchData 는 언제나 true 가 될 것이다.
결국 매번 데이터를 다시 페칭하게 된다.
진짜 클래스 컴포넌트로 이 수수께끼를 해결하는 방법은 이 꽉 깨물고 query 자체를 Child 컴포넌트에 넘기는 것 뿐입니다. Child 컴포넌트가 query 를 직접 사용하지 않음에도 불구하고 query 가 바뀔 때 다시 데이터를 불러오는 로직은 해결할 수 있습니다.
classParentextendsComponent{state={query: 'react'};fetchData=()=>{consturl='https://hn.algolia.com/api/v1/search?query='+this.state.query;// ... 데이터를 불러와서 무언가를 한다 ...};render(){return<ChildfetchData={this.fetchData}query={this.state.query}/>;}}classChildextendsComponent{state={data: null};componentDidMount(){this.props.fetchData();}componentDidUpdate(prevProps){if(this.props.query!==prevProps.query){this.props.fetchData();}}render(){// ...}}
클래스 컴포넌트에서, 함수 prop 자체는 실제로 데이터 흐름에서 차지하는 부분이 없습니다.
this 변수에 묶여 있기 때문에 함수의 일관성을 담보할 수 없게 됩니다.
부모 컴포넌트로부터 내려온 this.props.fetchData 가 어떤 상태에 기대고 있는지, 아니면 그냥 상태가 바뀌기만 한 것인지 알 수가 없습니다.
effect
useCallback 을 사용하면, 함수는 명백하게 데이터 흐름에 포함됩니다.
만약 함수의 입력값이 바뀌면 함수 자체가 바뀌고, 만약 그렇지 않다면 같은 함수로 남아있다고 말 할 수 있습니다. 세밀하게 제공되는 useCallback 덕분에 props.fetchData 같은 props 변화는 자동적으로 하위 컴포넌트로 전달됩니다.
그렇다고 useCallback 을 어디든지 사용하는 것은 꽤 투박한 방법이라고 강조하고 싶습니다.
만약 제가 {id: 10} 으로 데이터를 요청하고 {id: 20} 으로 바꾸었다면, {id: 20} 의 요청이 먼저 시작된다.
그래서 먼저 시작된 요청이 더 늦게 끝나서 잘못된 상태를 덮어씌울 수 있게된다.
이를 경쟁 상태라고 하며, (보통 비동기 호출의 결과가 돌아올 때까지 기다린다고 여기며) 위에서 아래로 데이터가 흐르면서 async / await 이 섞여있는 코드에 흔히 나타납니다. 여기서 데이터가 흘러간다는 말은 props나 state가 async 함수 안에서 바뀔 수 있다는 이야기입니다.
여러분이 사용하는 비동기 접근 방식이 취소 기능을 지원한다면 아주 좋습니다! 그러면 클린업 함수에서 바로 비동기 함수를 취소를 할 수 있습니다.
functionArticle({ id }){const[article,setArticle]=useState(null);useEffect(()=>{letdidCancel=false;asyncfunctionfetchData(){constarticle=awaitAPI.fetchArticle(id);if(!didCancel){setArticle(article);}}fetchData();return()=>{didCancel=true;};},[id]);// ...}
The text was updated successfully, but these errors were encountered:
Hook (useEffect)
모든 랜더링은 고유의 Props과 State가 있다
호출 시점
에 가지고 있던 props, state가 명시되어 있다.모든 랜더링은 고유의 이벤트 핸들러를 가진다
호출 시점
에 가지고 있던 props, state를 사용한다.모든 랜더링은 고유의 이펙트를 가진다
모든 랜더링은 고유의… 모든 것을 가지고 있다
흐름을 거슬러 올라가기
그러면 클린업(cleanup)은 뭐지?
라이프사이클이 아니라 동기화
리액트에게 이펙트를 비교하는 법을 가르치기
리액트에게 의존성으로 거짓말하지 마라
의존성으로 거짓말을 하면 생기는 일
의존성을 솔직하게 적는 두 가지 방법
이펙트가 자급자족 하도록 만들기
함수형 업데이트와 구글 닥스(Google Docs)
액션을 업데이트로부터 분리하기
왜 useReducer가 Hooks의 치트 모드인가
함수를 이펙트 안으로 옮기기
하지만 저는 이 함수를 이펙트 안에 넣을 수 없어요
함수도 데이터 흐름의 일부인가?
경쟁 상태에 대해
The text was updated successfully, but these errors were encountered: