함수 내에서 어떤 구현이 함수 외부에 영향을 끼치는 경우 해당 함수는 Side Effect가 있다고 이야기 한다.
React에서는 컴포넌트 내에서 fetch를 사용해 API 정보를 가져오거나 이벤트를 활용해 DOM 직접 조작할 때 Side Effect가 발생했다고 말한다.
다음은, 전역 변수 foo 를 bar라는 함수가 수정하는 예제이다.
let foo = 'hello';
function bar(){
foo = 'world';
}
var(); // bar는 Side Effect를 발생시킨다.
순수 함수란, 오직 함수의 입력만이 함수의 결과에 영향을 주는 함수를 의미한다. 함수의 입력이 아닌 다른 값이 함수의 결과에 영향을 미치는 경우, 순수 함수라고 부를 수 없다. 또한 순수 함수는, 입력으로 전달된 값을 수정하지 않는다.
function upper(str){
return str.toUpperCase(); / toUpperCase 메소드는 원본을 수정하지 않는다 (Immutable)
}
upper('hell')//'HELLO'
순수 함수에는 네트워크 요청과 같은 Side Effect가 없다. 순수 함수으 ㅣ특징 중 하나는, 어떠한 전달 인자가 주어질 경우, 항상 똑같은 값이 리턴됨을 보장한다. 그래서 예측 가능한 함수이기도 하다.
Q.
Math.random()
은 순수 함수가 아니다. 왜일까?우리가 앞서 배운 React의 함수 컴포넌트는, props가 입력으로, JSX Element가 출력으로 나간다. 여기에는 그 어떤 side Efeect도 없으며, 순수 함수로 작동한다.
function SingleTweet({writer, body, createdAt}){
return <div>
<div>{writer}</div>
<div>{createdAt}</div>
<div>{body}</div>
</div>
}
하지만 보통 React 애플리케이션을 작성할 때에는, AJAX 요청이 필요하거나, LocalStorage 또는 타이머와 같은 React와 상관없는 API를 사용하는 경우가 발생할 수 있다. 이는 React의 입장에서는 전부 Side Effect이다. React는 Side Effect를 다루기 위한 Hook인 Effect Hook을 제공한다.
여기 명언을 보여주는 간단한 애플리케이션이 있다. 먼저 이 링크를 열어서 버튼을 클릭할 때마다 브라우저 상단의 타이틀이 어떻게 변경되는지 확인해보자.
useEffect
는 컴포넌트 내에서 Side effect를 실행할 수 있게 하는 Hook이다. 이 컴포넌트에서 실행하는 Side effect는 브라우저 API를 이용하여, 타이틀을 변경하는 것 이다. 다음 코드를 확인해 보자.
useEffect(함수)
useEffect
의 첫 번째 인자는 함수이다. 해당 함수 내에서 side effect를 실행하면 된다. 이 함수는 다음과 같은 조건에서 실행된다.
언제 실행되나?
// ex)
useEffect(() =>{
console.log(몇 번 호출 될까요?)
}) // <-- 컴포넌트가 처음 생성되거나, props가 업데이트 되거나, state가 업데이트 될 때마다 실행된다.
useEffect(() =>{
console.log(몇 번 호출 될까요?)
},[]) // <-- 컴포넌트가 처음 생성될 때만 함수가 실행된다. 즉 최초 1회만 호출한다.
useEffect(() =>{
console.log(몇 번 호출 될까요?)
},[dep]) // <-- `dep`이 업데이트 될 때마다 실행된다.
이와 같이 매번 새롭게 컴포넌트가 렌더링 될 때 Effect Hook이 실행된다.
useEffect
의 두 번째 인자는 배열이다. 이 배열은 조건을 담고 있다. 여기서 조건은 boolean 형태의 표현식이 아닌, 어떤 값의 변경이 일어날 때를 의미한다. 따라서, 해당 배열엔 어떤 값의 목록이 들어간다. 이 배열을 특별히 종속성 배열이라고 부른다.
이 예제를 열어보고, 개발자 콘솔의 값을 확인해보자. 여기에는 다음과 같은 세 상태가 존재한다.
이 예제는, filter
가 변할 때에만, effect 함수가 실행된다. 개발자 콘솔을 통해 확인할 수 있다.
한편, 카운트를 올리는 버튼은 컴포넌트의 상태가 바뀌고 업데이트되지만, 아무리 버튼을 눌러도 effect함수는 실행되지 않는다. 왜냐하면, 종속성 배열에는 filter
만 존재하고, count
는 존재하지 않기 때문이다.
Q.
useEffect(함수,[종속성1, 종속성2, ...])
useEffect
의 두 번째 인자는 종속성 배열이다. 배열 내의 종속성1, 또는 종속성2의 값이 변할 때, 첫 번째 인자의 함수가 실행된다. 배열 내의 어떤 값이 변할 때에만, (effect가 발생하는) 함수가 실행된다.
만일 종속성 목록에 아무런 종속성도 없다면 어떤 일이 발생할까? 달리 말해, 두 번째 배열을 빈 배열[]
로 둘 경우에는 무슨 일이 발생할까? 두 번째 인자를 아예 안 넘기는 것과 어떻게 다를까?
- 빈 배열 넣기
useEffect(함수,[])
- 아무 것도 넣지 않기(기본 형태)
useEffect(함수)
(2번) 기본 형태의 useEffect
는 컴포넌트가 처음 생성되거나, props가 업데이트되거나, 상태(state)가 업데이트 될 때 effect 함수가 실행됨을 앞서 배웠다.
반면에 (1번) 빈 배열을 useEffect의 두 번째 인자로 사용하면, 이 때는 컴포넌트가 처음 생성될 때만 effect 함수가 실행된다. 이것이 언제 필요할까? 대표적으로 처음 단 한번, 외부 API를 통해 리소스를 받아오고 더 이상 API 호출이 필요하지 않을 때에 사용할 수 있다.
목록 내 필터링을 구현하기 위해서는 다음과 같은 두 가지 접근이 있을 수 있다.
처음 단 한 번, 외부 API로부터 명언 목록을 받아오고, filter 함수를 이용한다.
검색어가 바뀔 때마다, 외부 API를 호출한다. (앞선 조건부 실행 콘텐츠에서 봤던 예제와 동일하다.)
각각의 장단점은 무엇인가? 지금은 storageUtil.js
를 이용해 외부 API를 직접 구현했지만(LocalStorage API를 이용), 이는 서버 요청으로 대체 할 수 있다. 만일 서버에서 수십만 개의 명언을 제공한다고 가정해보자. 다음의 표는 HTTP를 이용한 서버 요청을 가정할 때, 두 방식으 ㅣ차이점을 설명하고 있다.
장점 | 단점 | |
---|---|---|
컴포넌트 내부에서 처리 | HTTP 요청의 빈도를 줄일 수 있다. | 브라우저(클라이언트)의 메모리 상에 많은 데이터를 갖게 되므로, 클라이언트의 부담이 늘어난다. |
컴포넌트 외부에서 처리 | 클라이언트가 필터링 구현을 생각하지 않아도 된다. | 빈번한 HTTP 요청이 일어나게 되며, 서버가 필터링을 처리하므로 서버가 부담을 가져간다. |
임의로 구현한 storageUtil.js
대신,fetch API를 써서, 서버에 요청한다면 코드는 어떻게 될까? 명언을 제공하는 API의 엔드포인트가 http://서버주소/proverbs
라고 가정해보자.
useEffect(() =>{
fetch(`http://서버주소/proverbs?q=${filter}`)
.then(resp => resp.json())
.then(result =>{
setProverbs(result);
});
}, [filter]);
모든 네트워크 요청이 항상 즉각적인 응답을 가져다주는 것은 아니다. 외부 API접속이 느릴 경우를 고려하여, 로딩화면의 구현은 필수적이다.
[그림] loading indicator & placeholder
기본적으로, Loading indicator의 구현은 어떻게 처리할 수 있을까? 여기에도 상태 처리가 필요하다.
const [isLoading, setIsLoading] = useState(true);
//생략, LoadingIndicator 컴포넌트는 별도로 구현했음을 가정한다.
return {isLoading ? <loadingIndicator /> :<div>로딩 완료 화면</div>}
fetch 요청의 전후로 setIsLoading
을 설정해 주어 보다 나은 UX를 구현할 수 있다.
usEffect (()=>{
setIsLoading(true);
fetch(`http://서버주소/proverbs?q=${filter}`)
.then(resp => resp.json())
.then(result => {
setProverbs(result);
setIsLoading(false);
});
},[filter]);