본문 바로가기

React/Hooks

React Hooks #1 - Motivation

Hooks를 아직도 잘 모르는 듯 해서 계속 공부를 해야겠습니다.

React

React Hooks에 대해서 말씀드리기 앞서서, react는 다음과 같은 방향을 가지고 있습니다.

 

1. 선언형

'선언'을 한다는 것은 소스 작성 의도를 쉽게 파악하고 사용 / 수정 / 개선을 용이하게 할 수 있습니다.

예를 들어서 리스트가 존재할 때 매 3번째 원소는 두 번 이어서 만들고 싶다면, 두 가지 접근법을 택할 수 있습니다.

1) 명령형(Imperative)

 

// imperative solution

const inputArray = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'];
let outputArray = [];

for(let i = 0; i < inputArray.length; i += 1) {
	let temp = inputArray[i];
	if(i % 3 === 0)
		temp = inputArray[i] + inputArray[i];
	outputArray.push(temp);
}

 

2) 선언형(Declarative)

 

// declarative solution

const inputArray = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'];
const outputArray = inputArray.map((value, index) => (index % 3 ? value : value + value));

 

특정 상황에 따라서 선언형 코드보다 명령형 코드가 강력할 수 있지만, 보통의 경우 어떤 작업을 할 것인지(해당 상황의 경우로는 배열을 수정하겠다 -> map function)에 대한 선언을 하는 것을 통해서 보다 명확하고 간단하게 작성할 수 있습니다. 다만, 복합적으로 사용할 수도 있기 때문에 복잡한 상황에 놓여 있다면 어느 수준까지 어떤 방식을 택할 것인지에 대한 고민이 필요할 수 있겠습니다.

 

2. 컴포넌트 기반

React는 각 컴포넌트에 대해서 해당 내부의 state와 view를 캡슐화하는 것을 추천합니다. 이를 통해서 각 컴포넌트는 각자 자신의 상태와 형태에 집중할 수 있습니다. 엘리먼트들을 사용자 인터페이스를 생각해서 재사용 가능한 조각으로 합치거나 나눌 수 있는지 고려하는 것이 필요한데 이를 컴포넌트라고 합니다. 이해를 위해서 React에서는 이미 deprecated 된 createClass(https://ko.reactjs.org/blog/2017/04/07/react-v15.5.0.html#migrating-from-reactcreateclass)를 통해서 컴포넌트를 만드는 방식을 소개드릴까 합니다.

 

const MyList = React.createClass({
	displayName: 'MyList',
	render() {
		return React.createElement('ul', { className: 'myList' },
			this.props.itemList.map((value, index) =>
				React.createElement('li', { key: index }, value)
			)
		)
	}        
});

 

해당 방식 이외에도 Class / Function Component(https://reactjs.org/docs/components-and-props.html)를 사용하는 것이 일반적입니다. 함수형 컴포넌트를 사용할 때 비구조화 할당(https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment#object_destructuring)을 사용하는 것을 통해서 props에 대한 코드 작성을 더욱 가독성 있게 할 수 있습니다.

 

const MyList = ({ itemList }) => (
	<ul className="myList">
		{itemList.map((value, index) =>
			<li key={index}>{value}<li>
		)}
	</ul>
);

export default MyList;

 

3. DRY (Don't Repeat Yourself)

React는 컴포넌트 기반의 작성을 장려하는 것으로, 소스 코드의 재사용성을 증대합니다. 순수 함수란 파라미터에 의해서만 반환 값이 결정되는 함수를 의미합니다. 즉 외부의 변화와 상관없이 인자가 같으면 항상 같은 값이나 함수를 반환하는 것을 말합니다. 리액트에서는 UI를 순수 함수로 표현합니다. 이러한 작성을 통해서 작동 방식에 대한 의심 없이 재사용을 자신 있게 할 수 있으며 해당 사용에 있어서 테스트 코드 작성을 명확하게 할 수 있습니다. https://www.caydenberg.io/blog/dry-react.html 해당 글에 Mixin / Inheritance / subcomponent / children / render props / high order component 등 반복 작업을 줄이고 유연성을 가지기 위한 방법들이 소개되어 있으니 한번 참고하시면 좋겠습니다.

This

Hooks의 도입 이전까지는 클래스형 컴포넌트에 life cycle methods와 this.setState(https://reactjs.org/docs/state-and-lifecycle.html), 즉 컴포넌트 내부의 상태를 관리하기 위한 methods를 사용해서 소스 코드를 작성했습니다. 이러한 사용에 있어서 가장 중요해진 부분은 this context입니다. this는 scope chain을 위한 lexical environment와 다르게 선언된 위치가 아니라, 호출하는 방식에 영향을 받습니다. this는 자신이 속한 객체나 생성할 인스턴스를 가리키는 자기 참조 변수(https://en.wikipedia.org/wiki/Self-reference)입니다. 함수를 선언하거나 사용할 때 arguments를 지역 변수처럼 사용하는 것과 같이 this를 지역 변수로 사용할 수 있는데, 앞서 말씀드린 것처럼 선언된 위치가 아니라 호출하는 방식에 의해서 동적으로 자신이 속한 객체 또는 자신이 생성할 인스턴스를 가리키게 됩니다.

1-1. normal function call : global object

1-2. normal function call with strict mode : undefined

2. method call : the object calls the method

3. creator function call : the instance that creator function will create

4-1. Function.prototype.apply : 1st parameter

 

Function.prototype.apply(thisArg[, argsArray]);

function applyFunction() {
//	console.log(`this name is ${this.name}`);
	console.log(`arguments are ${arguments}`)
	return this;
}

const applyThis = {
	name: 'ansrlm'
};

const returnedThis = applyFunction.apply(applyThis, ['my', 'arguments']);
// -> arguments are Arguments(2) ['my', 'arguments', callee ...

console.log(`this name is ${returnedThis.name}`);
// -> this name is ansrlm

 

4-2. Function.prototype.call : 1st parameter

apply / call의 경우 this로 사용할 객체를 전달함과 동시에 함수를 호출합니다. apply가 함수 내부에서 인자로 사용하고자 전달할 객체를 하나의 array로 보낸다면 call은 해당 부분들을 2nd paramter부터 arguments로 전달합니다.

 

Function.prototype.call(thisArgs[, arg1[, arg2[, [arg3, ...]]]]);

 

4-3. Function.prototype.bind : 1st parameter

bind는 apply / call method와 다르게 함수를 호출하지 않고 this로 사용할 객체를 전달합니다. 그래서 unit test code를 작성할 때 callback function으로 작성되어 있는 일반 함수에 대해서 mocking을 할 때 효과적으로 적용할 수 있습니다.

 

describe('mocking with bind', () => {
	let sourceFunction;

	before(() => {
		sourceFunction = jest.fn(function (name) {    
			this.name = name;
		});
	});

	it('bind method will bypass eslint error', () => {
		const creatorFunction = new sourceFunction('destination');
        
		const thisArgs = {
			name: 'arguments'        
		};
		const bindFunction = sourceFunction.bind(thisArgs);

		console.log(sourceFunction.mock.instances);
	});
})

Classes

소개드린 부분처럼, this가 가리키는, 참조하는 부분은 상황에 따라서 다릅니다. 그래서 특정 상황에 따라서 해당 부분을 조정하는 작업이 필요합니다. 이 행위는 매우 중요하며 어려운 부분으로 Class object를 re-bind 하는 것은 어떤 부분을 수정하고 어떤 의도에 따라서 변경을 하는지 tracking을 해야 합니다. 그래서 사용하지 않는 부분의 삭제와 성능 최적화가 어려울 수 있습니다.

이에 더해서, classes는 종종 중복된 코드들을 작성해야 할 가능성이 높습니다. 이는 class lifecycle에서 쉽게 찾아볼 수 있습니다. 첫 화면을 그릴 때 전체 도서 목록이 나오고, 필터 조건에 따라서 조건에 부합한 도서 목록을 가져오는 상황을 가정해 보겠습니다. 이런 상황에서 react lifecycle은 다음과 같이 구성할 수 있습니다.

 

const BASE_URL = 'https://ansrlm.tistory.com';

const BOOKS = 'books';

class BookList extends React.PureComponent {

...

	componentDidMount() {
		fetch(`${BASE_URL}/${BOOKS}?name=${this.props.name}`)
		.then((response) => {
			this.setState(response);		    
		});
	}

...

}

 

첫 화면을 구성하기 위해서 componentDidMount를 사용했다면, props가 바뀌는 경우에 대해서 name이 바뀌지 않은 경우를 확인한 이후에 도서 목록을 가져오도록 할 수 있습니다.

 

...

	componentDidUpdate(prevProps) {
		if(this.props.name !== prevProps.name) {
			fetch(`${BASE_URL}/${BOOKS}?name=${this.props.name}`)
			.then((response) => {
				this.setState(response);		    
			});
		}	
	}

...

 

중복된 코드를 줄이기 위해서 fetch function을 하는 부분이 중복이 되기 때문에 해당 부분을 함수로 분리할 수 있습니다.

 

const BASE_URL = 'https://ansrlm.tistory.com';

const BOOKS = 'books';

class BookList extends React.PureComponent {

...

	fetchBookList() {
		fetch(`${BASE_URL}/${BOOKS}?name=${this.props.name}`)
		.then((response) => {
			this.setState(response);		    
		});
	}

	componentDidMount() {
		this.fetchBookList();
	}

	componentDidUpdate(prevProps) {
		if(this.props.name !== prevProps.name) {
			this.fetchBookList();
		}	
	}

...

}

 

이처럼 데이터를 가져오는 작업을 componentDidMount와 componentDidUpdate가 수행하지만 componentDidMount에서 이벤트 리스너를 설정하는 것과 같은 로직이 있어야 할 수도 있습니다. 이렇게 함께 변경되는 상호 관련 코드는 분리할 수 있지만 연관 없는 코드들은 단일 메서드로 작성하게 되고 관리 포인트가 늘어나며 이로 인해서 버그가 쉽게 발생합니다. 컴포넌트 간 소스 공유를 위해서 lifecycle을 같이 사용하는 부분에 HOC(high order component)를 사용할 수 있습니다. 그러나 함수를 분리한다고 하더라도 결국은 두 곳에서 호출하는 것은 변함이 없으며, HOC의 경우 Wrapper Hell에 빠질 수 있습니다.

Wrapper Hell

Context(https://reactjs.org/docs/context.html)를 사용할 경우에 Wrapper Hell에 빠지는 경우가 많습니다. Context를 이용하면 단계마다 일일이 props를 넘겨주지 않고도 컴포넌트 트리 전체에 데이터를 제공할 수 있습니다. React Component Tree에서 전역으로 사용하는 (EX. Authentication / Theme / Language / ...) 데이터를 트리 최 하단에 전달하는 경우 props가 아닌 특정 공간에서 전달하는 것을 가능하게 합니다.

 

<AuthenticationContext.Consumer>
	{(user) => {
		if(user)
			return (
				<ThemeContext.Consumer>
					{(theme) => (
							<LanguageContext.Consumer>
								{(language) => (
										<>	
											...
										</>
									)
								};
							<LanguageContext.Consumer/>
						)
					};
				<ThemeContext.Consumer/>
			);
		else 
			return (
				<ErrorPage/>
			);
		}
	};
<AuthenticationContext.Comsumer/>

 

해당 방식은 꼭 필요하지만 depth가 깊어지고 provider-consumer, HOC, render props, 다른 추상화에 대한 layer들이 많이 필요하게 되면 React devtools에서 디버깅을 하려고 하면 엄청나게 길고 복잡한 트리를 보게 됩니다. 또한 PropTypes, defaultProps, 그리고 HOCs와 같은 static 선언은 tree-shaking에 대상이 되지 않을 수 있어서 번들의 용량이 커질 수 있습니다.

Hooks

이러한 상황에 대해서 Hooks는 리액트의 원칙을 수용하면서 더욱 가독성 있는 코드를 작성할 수 있게 도와줍니다. props, state, context, refs, lifecycle, ... 의 React 개념에 더욱 직관적인 API를 사용할 수 있습니다. 앞서 작성했었던 도서 관리 페이지에 대해서 Hooks를 사용하게 되면 lifecycle method를 기반으로 나누는 것보다는 Hooks를 통해서 서로 비슷한 기능을 하는 작은 함수의 묶음으로 컴포넌트를 나눌 수 있습니다. lifecycle method를 hooks로 치환하거나 대응해서 이해할 수 있지만, React Hooks는 다른 mindset으로 상태 관리를 접근합니다.

Hooks 사용 규칙

Hooks는 매우 유연하고 자바스크립트 함수의 일종이지만 명확한 두 가지 규칙이 있습니다.

1. 최상위에서 호출 - Hook선언적 관점을 위해서 언제나 같게 유지해야 합니다. 즉 if / loop / nestied function에서 실행하지 않아야 합니다.

2. Function Components에서 사용 - 일반 자바스크립트 함수 내에서, 그리고 Class Components 내에서 호출을 하더라도 실행되지 않습니다.

이 규칙을 지키기 위해서 eslint rule(https://www.npmjs.com/package/eslint-plugin-react-hooks)이 있습니다.

 

 

 

※ 잘못된 부분이나 궁금한 점 댓글로 작성해 주시면 최대한 답변하겠습니다. 읽어주셔서 감사합니다.

※ React Hooks #2 - Hooks(useReducer)에서 useReducer 사용에 대해서 소개하고 useEffect의 의존성 배열에 대한 이야기를 드리겠습니다.

'React > Hooks' 카테고리의 다른 글

React Hooks #5 - Hooks(useRef)  (0) 2021.09.24
React Hooks #4 - Hook Flow  (0) 2021.09.22
React Hooks #3 - Hooks(useContext)  (0) 2021.08.28
React Hooks #2 - Hooks(useReducer)  (0) 2021.08.16