리액트의 속성에 대해서 이야기하면 리액트의 하향식 단방향 데이터 흐름에 대해서 간단하게 설명했었다. 그런데 여기서 발생할 수 있는 문제가 한 가지 있다. 만약 부모 컴포넌트에서의 상태가 하위 컴포넌트의 어떤 이벤트로 인해 변경되야 한다면 어떻게 구현해야하는가의 문제이다. 이런 문제를 해결하는 방법을 상태 끌어올리기라고 부른다. 간단하게 설명하자면 상태를 변경시키는 함수를 하위 컴포넌트의 속성으로 전달해서 사용하게 만드는 방법이다.
위의 예시에서는 부모 컴포넌트에 상태 값 하나가 있고 그 상태를 갱신할 수 있는 함수가 하나 있다. 그 밑에 자식 컴포넌트에 속성으로 해당 함수를 넘겨주고 있고 자식 컴포넌트에서 해당 함수를 실행시키는 함수를 하나 작성했다. 그리고 이 함수를 버튼의 클릭 이벤트 리스너에 연결해줬다. 이제 버튼을 눌러보면 부모 컴포넌트의 상태가 변경되는 것을 확인할 수 있다. 이름만 들어서는 리액트의 단방향 데이터 흐름을 어기는 것 같지만 실제로는 상태를 변경할 수 있는 함수를 부모 컴포넌트에서 자식 컴포넌트로 전달했고 자식 컴포넌트는 그냥 그 함수를 사용하기만 했으므로 여전히 단방향 데이터 흐름은 유지되고 있다.
순수 함수(Pure Function)는 오직 함수의 입력만이 함수의 결과에 영향을 주는 함수이며 같은 입력에는 항상 같은 반환 값을 가지는 함수이다. 리액트에서는 네트워크 요청이나 DOM 조작 없이 속성을 입력으로 받아서 JSX 요소들을 출력하는 간단한 함수형 컴포넌트 정도가 순수 함수에 속한다. 한편 함수 내의 어떤 구현이 함수 외부에 영향을 끼치는 경우 해당 함수는 부수 효과(Side Effect)가 있다고 한다. 리액트에서 AJAX 요청이 필요하거나 리액트와 상관없는 API 등을 사용하는 경우는 전부 이 부수 효과가 있는 경우이며 이것을 다루기 위한 기능들이 바로 Effect Hook이다. useEffect 함수는 해당 컴포넌트가 렌더링 되는 순간 컴포넌트 내에서 부수 효과를 실행할 수 있게 하는 훅이다.
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
}, [props.friend.id]);
위의 코드는 리액트 공식 홈페이지에서 Effect Hook을 사용하는 예시로 등장하는 코드이다. 자세히 보면 Effect Hook에는 2개의 인자가 존재하는 것을 확인할 수 있다. 첫 번째 인자는 함수의 형태를 가지고 있는데 여기에 실행하고자 하는 부수 효과가 있는 함수를 작성하면 된다. 해당 함수는 반환 값을 가질 수도 그렇지 않을 수도 있는데 이것은 정리(clean-up)이 필요한 경우 그 정리를 위한 코드를 반환하면 된다. 이 첫 번째 인자에 적힌 함수는 실제로 컴포넌트가 렌더링 혹은 리렌더링 되는 순간인 처음 컴포넌트가 생성되는 시점과 컴포넌트의 상태가 바뀌거나 새로운 속성이 전달되는 경우에 실행될 것이다. 그런데 이 실행 시점을 바꿀수도 있다.
두 번째 인자에 적힌 배열은 의존성 배열로 선택적인 요소로 적지 않을 수도 있으나 만약 적는다면 이 배열에 적혀있는 값에 변경이 일어날 때만 Effect Hook이 실행되게 만든다. 따라서 의존성 배열을 사용하면 해당 컴포넌트가 처음 렌더링 될 때를 제외하면 의존성 배열의 값이 바뀔때만 부수 효과가 있는 함수가 실행되도록 일종의 조건문과 같은 기능을 구현할 수 있다. 참고로 의존성 배열을 적되 빈 배열로 적는다면 이 Effect Hook은 오직 처음 렌더링 될 때만 실행되는 경우에 사용할 수 있다.
사실 지금 사용하고 있는 함수형 컴포넌트는 리액트 16.8 버전에서 새롭게 도입된 것이고 그 이전에는 클래스형 컴포넌트라는 것을 사용했다. 그때는 생명주기 메서드라는 것을 사용해서 컴포넌트를 마운트하고 필요한 내용을 업데이트 한 뒤에 제거할 때는 언마운트하는 과정을 거쳤어야 했으나 현재는 생명주기 메서드 자체가 사용이 제한되어 있고 비슷한 기능을 이 Effect Hook을 통해 구현하고 있다.
그렇다면 대략적인 Effect Hook의 실제 사용 모습을 예시와 함께 알아보자.
위의 코드는 Effect Hook을 사용해서 외부에서 데이터를 호출하는 예시이다. 물론 예시에서는 그냥 다른 파일에서 localStorage에 JSON 문자열을 저장하고 그것을 문자열의 형태로 반환하는 함수를 구현해서 사용하는 것이지 실제 외부 API에 네트워크 요청을 한 것은 아니라는 것을 밝힌다. 다른 부분은 앞서 다른 포스팅에서 사용했던 부분들을 약간 수정한 것이라서 따로 설명하지는 않고 Effect Hook 부분만을 자세히 보려고 한다. useEffect를 사용해서 컴포넌트가 마운트 될 때 실행되는 부수 효과를 정의하고 있다. 단 의존성 배열은 빈 배열이기 때문에 오로지 마운트 될 때 딱 한 번만 실행된다. 그래서 input 창에 어떤 내용을 입력하거나 삭제해도 useEffect문 앞에 "Effect Hook이 실행되었습니다."라고 콘솔에 출력하는 명령어는 한번 밖에 실행되지 않는다. 호출한 데이터를 해당 컴포넌트가 관리하는 상태값으로 설정하는 것이 예시 코드에서 Effect Hook의 사용법이다.
참고로 필터에 해당하는 기능을 구현했기 때문에 추가적으로 설명하자면 위의 예시는 모든 데이터를 호출해서 클라이언트 상에서 필터를 구현하는 방식이다. 이렇게 구현하면 네트워크 요청을 줄일 수 있지만 클라이언트의 메모리 상에 모든 정보를 받기 때문에 클라이언트 부담이 늘어나는 방식이다. 반면 클라이언트에서 필터링 관련 내용을 구현하지 않고 필터링할 값에 따라 매번 서버에 요청하는 방식으로 구현할 수도 있다. 이렇게 하면 따로 클라이언트가 필터링을 구현하지 않아도 되지만 네트워크 요청이 훨씬 빈번해지고 서버에 더 많은 부담을 주는 방식이다. 일반적으로 데이터의 양이 작고 단순한 필터링 정도만 있다면 클라이언트에서 처리할 수 있지만 데이터 양이 많아지거나 필터링 자체가 복잡한 경우는 보통 서버에서 필터링을 처리하는 것이 성능과 보안 측면에서 유리하다고 알려져 있다.