자바스크립트 개발자 포럼에서 개최한 JSCONF 2020의 세번째 세션인 슬기로운 HOOK 생활 : 순수함수에서 REACT HOOK으로 떠나는 여행 - 이종은을 정리하였습니다. 시간이 되신다면 선언적 UI를 다루는 맨 앞의 원지혁님의 세션도 보시는 것을 권장합니다.
입력을 받아서, 출력을 만드는 것
let c = 1;
function add(a, b){
return a + b + c;
}
add(1, 3);
전역변수인 c가 출력에 영향을 미치고 있습니다. 그러나 인자로 주지 않아서 c는 입력으로 포함되고 있지 않습니다. 따라서 해당 함수는 불순하다고 말할 수 있습니다.
let count = 0;
function add(a, b){
count++;
return a + b;
}
add(1, 2);
해당 함수는 입력이 같을 때 출력은 같아서 첫번째 조건은 만족하나, 두번째 조건을 만족하지는 않습니다. 함수의 실행으로 인하여 외부 scope의 변수인 count의 값이 변화하였기 때문입니다. 해당 함수로 인하여 함수 외부 세계(바깥 scope)가 영향을 받아 부수작용이 있습니다.
let count = 0
function getName(obj){
obj.number = undefined;
return obj.name;
}
let contact = {
name: 'TK',
number: '010-0000-0000'
};
getName(contact);
해당 코드는 함수 안에서 obj 객체의 값을 바꾸고 있습니다. 따라서 불순(impure)한 함수입니다. 함수 외부의 것에 입력도 포함된다는 것을 알아둡시다.
function add(a, b){
return a + b;
}
return(1, 2);
입력이 같으면 출력이 같고, 함수 외부의 어떤 것에도 영향을 주지 않기에 순수함수라고 부를 수 있습니다.
function arrOp(arr, callback){
return callback(array);
}
arrOp([1,2,3,4,5], (arr)=>arr.pop());
arrOp는 callback을 실행하는데, callback은 arrOp의 인자인 arr을 pop하여 마지막 요소를 제거합니다. 이 때 arrOp 순수함수입니다. 인자함수인 callback에 의해 외부 scope가 영향을 받았다면 이는 불순한 것으로 간주하지 않기 때문입니다.
코드의 실행이 예측 가능합니다.
이와 더불어 테스트 코드의 작성도 수월해집니다.
어떠한 상태도 기억하지 못합니다. stateless합니다. 그래서 react에서는 입력(props)와는 다른 상태 값을 component가 지니게 만들었습니다. 이는 React에서 알아보겠습니다.
리액트 프레임워크 제작자들은 순수함수를 컴포넌트에 적용하여 react를 개발하였습니다.
모든 리액트 컴포넌트는 props에 대하여 순수함수처럼 동작해야 한다.
리액트에서 props는 부모 컴포넌트가 자식 컴포넌트에게 주는 값이며 자식 컴포넌트에서는 바꿀 수 없는 값입니다. 자식 컴포넌트를 함수 내부라 하고 부모를 외부라 하면 자식의 컴포넌트는 props를 바꾸지 못하여 순수 함수의 성질을 지녔다고 할 수 있겠습니다. 이는 컴포넌트의 실행을 예측하기 위해 의도된 것이다. 리액트에서는 one-way-data-flow로 데이터가 한 부모 컴포넌트에서 자식 컴포넌트로 흐르는 특징을 지녔는데 이것도 순수함수와 같은 맥락입니다.
import ReactDOM from "react-dom";
import React, { Component } from "react";
import Child from "./child";
class Parent extends Component {
constructor(props) {
super(props);
this.state = {
name: "hello"
};
this.changeName = this.changeName.bind(this);
}
changeName() {
this.setState({
name: "world"
});
}
render() {
return (
<div>
<Child changeName={this.changeName} name={this.state.name} />
</div>
);
// return <button onClick={this.changeName}>{this.state.name}</button>;
}
}
ReactDOM.render(<Parent />, document.getElementById("root"));
Parent는 Child에게 state를 props로 전달하고 있습니다. 이 때 state의 값을 변경시키는 함수까지 Child에게 전달하여 순수함수의 원칙에 위배되지 않도록 (child가 parent에게 직접적으로 접근하지 않도록) 만들었습니다.
import React, { Component } from "react";
class Child extends Component {
constructor(props) {
super(props);
}
render() {
return <button onClick={this.props.changeName}>{this.props.name}</button>;
}
}
export default Child;
Child 컴포넌트는 함수에 대응되는 input과 마찬가지로 props 또는 부모의 값을 변경시키지 않고 그저 클릭될 때 함수를 호출하고 있습니다. 따라서 순수함수적 특징을 지니고 있다고 볼 수 있습니다.
인풋(props)를 컴포넌트가 사용하는 곳인 부모에서 지정해줄 필요가 있을 경우에는 props로 받으면 됩니다. 그러나 부모가 지정해줄 필요가 없을 경우에는 굳이 관련 함수를 부모에서 정의할 필요가 없습니다. 그래서 자식에서 setState를 쓰면 되겠죠.
사용자 입력을 받는 component를 작성해보겠습니다.
Child.js
import React, { Component } from "react";
class Child extends Component {
constructor(props) {
super(props);
}
render() {
return (
<div>
<h1>{this.props.title}</h1>
<input onChange={this.props.changeName} value={this.props.name} />
</div>
);
}
}
export default Child;
import ReactDOM from "react-dom";
import React, { Component } from "react";
import Child from "./child";
class Parent extends Component {
constructor(props) {
super(props);
this.state = {
name: "hello"
};
this.changeName = this.changeName.bind(this);
}
changeName(e) {
this.setState({
name: e.target.value
});
}
render() {
return (
<div>
<Child
title="오늘 할 일"
name={this.state.name}
changeName={this.changeName}
/>
</div>
);
// return <button onClick={this.changeName}>{this.state.name}</button>;
}
}
ReactDOM.render(<Parent />, document.getElementById("root"));
입력과 관련된 상태값은 부모와 관계가 없어 자식으로 옮긴다고 가정해보겠습니다.
Child.js
import React, { Component } from "react";
class Child extends Component {
constructor(props) {
super(props);
this.state = {
name: this.props.name,
hasTooManyCharacters: ""
};
this.changeName = this.changeName.bind(this);
}
changeName(e) {
this.setState({
name: e.target.value
});
if (e.target.value.length >= 10) {
this.setState({
hasTooManyCharacters: "Too many characters for one input area."
});
} else {
this.setState({
hasTooManyCharacters: ""
});
}
}
render() {
return (
<div>
<h1>{this.props.title}</h1>
<input onChange={this.changeName} value={this.state.name} />
<p>{this.state.hasTooManyCharacters}</p>
</div>
);
}
}
export default Child;
component가 복잡해질수록 ui를 그리는 로직과 거리가 먼 함수들이 component에 들어가 코드가 복잡해집니다. 따라서 React 개발자들은 상태를 지니는 함수형 컴포넌트인 hook을 개발하였습니다.
import React, { useState, useEffect } from "react";
const useName = initialName => {
const [name, setName] = useState(initialName);
return [name, setName];
};
const useInputWarning = initialValue => {
const [inputWarning, setInputWarning] = useState("");
const validateInput = charLength => {
if (charLength <= 2) {
setInputWarning("이름이 너무 짧아요");
} else if (charLength >= 10) {
setInputWarning("이름이 너무 길어요");
} else {
setInputWarning("적절한 이름입니다.");
}
};
return [inputWarning, validateInput];
};
const Child = ({ title, initialName }) => {
const [name, setName] = useName(initialName);
const [inputWarning, validateInput] = useInputWarning("");
return (
<div>
<h1>{title}</h1>
<div>
<input
onChange={event => {
setName(event.target.value);
}}
value={name}
/>
</div>
<button onClick={() => validateInput(name.length)}>확인</button>
<div>{inputWarning}</div>
</div>
);
};
export default Child;
해당 component와 관련된 상태값들을 넣을 수 있으면서 해당 로직을 실질적으로 ui를 그리는 부분과 분리하면서 코딩이 한결 편해졌습니다. 점점 함수가 어떻게(how)하는지가 아닌 무엇을(what)하는지에 가까워지면서 declarative programming에 가까워지게 됩니다.