-
TIL_200526 (What is React)Today I Learned 2020. 5. 27. 00:25
Handling Events
React 엘리먼트에서 이벤트를 처리하는 방식은 DOM 엘리먼트에서 이벤트를 처리하는 방식과 매우 유사합니다.
React의 이벤트는 소문자 대신 camelCase를 사용합니다.
JSX를 사용하여 문자열이 아닌 함수로 이벤트 핸들러를 전달합니다.
- HTML <button onclick="activateLasers()"> Activate Lasers </button> - React <button onClick={activateLasers}> Activate Lasers </button>
React에서는 기본 동작을 방지하기 위해서 반드시 preventDefault를 명시적으로 호출해야 합니다.
- HTML <a href="#" onclick="console.log('The link was clicked.'); return false"> Click me </a> - React function ActionLink() { function handleClick(event) { event.preventDefault(); console.log('The link was clicked.'); } return ( <a href="#" onClick={handleClick}> Click me </a> ); }
React에서는 addEventListener를 호출할 필요없이 엘리먼트가 처름 렌더링될때 리스너를 제공하면 됩니다.
이벤트 핸들러를 클래스의 메서드로 만들어 사용하는 것입니다.
class Toggle extends React.Component { constructor(props) { super(props); this.state = {isToggleOn: true}; // 콜백에서 `this`가 작동하려면 아래와 같이 바인딩 해주어야 합니다. this.handleClick = this.handleClick.bind(this); } handleClick() { this.setState(state => ({ isToggleOn: !state.isToggleOn })); } render() { return ( <button onClick={this.handleClick}> {this.state.isToggleOn ? 'ON' : 'OFF'} </button> ); } } ReactDOM.render( <Toggle />, document.getElementById('root') );
JSX 콜백 안에서 this의 의미에 대해 주의해야합니다.
Javascript에서 클래스 메서드는 기본적으로 바인딩되어 있지 않습니다.
this.handleClick을 바인딩하지 않고 onClick에 전달하였다면, 함수가 실제 호출될대 this는 undefined가 됩니다.
이것은 react만의 특수한 동작이 아니고 Javascript에서 함수가 작동하는 방식의 일부입니다.
만약에 바인딩하는 것이 불편하다면 두가지 방법으로 대체할 수 있습니다.
퍼블릭 클래스 필드 문법
class LoggingButton extends React.Component { // 이 문법은 `this`가 handleClick 내에서 바인딩되도록 합니다. // 주의: 이 문법은 *실험적인* 문법입니다. handleClick = () => { console.log('this is:', this); } render() { return ( <button onClick={this.handleClick}> Click me </button> ); } }
화살표 함수를 사용
class LoggingButton extends React.Component { handleClick() { console.log('this is:', this); } render() { // 이 문법은 `this`가 handleClick 내에서 바인딩되도록 합니다. return ( <button onClick={() => this.handleClick()}> Click me </button> ); } }
화살표 함수를 사용하는 방법의 문제점은 LoggingButton이 렌더링될 때마다 다른 콜백이 생성됩니다.
콜백이 하위 컴포넌트에 props로서 전달된다면 그 컴포넌트들은 추가로 다시 렌더링을 수행할 수도 있습니다.
이벤트 핸들러에 인자 전달하기
루프 내부에서는 이벤트 핸들러에 추가적인 매개변수를 전달하는 것이 일반적입니다. 예를 들어, id가 행의 ID일 경우 다음 코드가 모두 작동합니다.
<button onClick={(event) => this.deleteRow(id, event)}>Delete Row</button> <button onClick={this.deleteRow.bind(this, id)}>Delete Row</button>
화살표 함수와 Function.prototype.bind를 사용합니다. 화살표 함수를 사용하면 명시적으로 인자를 전달해야 하지만 bind의 경우 추가인자가 자동으로 전달됩니다.
Conditional Rendering
React에서는 원하는 동작을 캡슐화하는 컴포넌트를 만들 수 있습니다. 그로인해 어플리케이션의 상태에 따라서 컴포넌트 중 몇개만을 렌더링할 수 있습니다.
Javascript의 조건처리와 같이 동작하여 if나 조건부 연산자를 사용할 수 있습니다. 그렇다면 React로 현재 상태에 맞게 UI를 업데이트할 수 있을 것입니다.
사용자의 로그인 상태에 맞게 두가지의 컴포넌트 중 하나를 보여주는 Greeting 컴포넌트를 만듭니다.
isLoggedIn props에 따라서 다른 인사말을 렌더링합니다.
function UserGreeting(props) { return <h1>Welcome back!</h1>; } function GuestGreeting(props) { return <h1>Please sign up.</h1>; } function Greeting(props) { const isLoggedIn = props.isLoggedIn; if (isLoggedIn) { return <UserGreeting />; } return <GuestGreeting />; } ReactDOM.render( // Try changing to isLoggedIn={true}: <Greeting isLoggedIn={false} />, document.getElementById('root') );
엘리먼트 변수
엘리먼트를 저장하기 위해 변수를 사용할 수 있습니다. 출력의 다른 부분은 변하지 않은 채로 컴포넌트의 일부를 조건부로 렌더링 할 수 있습니다. 아래의 컴포넌트는 현재 상태에 맞게 <LoginButton />이나 <LogoutButton />을 렌더링 합니다. 또한 <Greeting />도 함께 렌더링합니다.
class LoginControl extends React.Component { constructor(props) { super(props); this.handleLoginClick = this.handleLoginClick.bind(this); this.handleLogoutClick = this.handleLogoutClick.bind(this); this.state = {isLoggedIn: false}; } handleLoginClick() { this.setState({isLoggedIn: true}); } handleLogoutClick() { this.setState({isLoggedIn: false}); } render() { const isLoggedIn = this.state.isLoggedIn; let button; if (isLoggedIn) { button = <LogoutButton onClick={this.handleLogoutClick} />; } else { button = <LoginButton onClick={this.handleLoginClick} />; } return ( <div> <Greeting isLoggedIn={isLoggedIn} /> {button} </div> ); } } ReactDOM.render( <LoginControl />, document.getElementById('root') );
if를 사용하여 조건부로 렌더링 하는 방법보다 짧은 구문을 사용하고 싶다면 JSX안에서 인라인(inline)으로 처리하는 몇가지 방법이 있습니다.
논리 &&연산자로 If를 인라인으로 표현하기
JSX 안에서는 중괄호를 이용해서 표현식을 포함할 수 있습니다. 논리 연산자 &&를 사용하여 쉽게 엘리먼트를 조건부로 넣을 수 있습니다.
function Mailbox(props) { const unreadMessages = props.unreadMessages; return ( <div> <h1>Hello!</h1> {unreadMessages.length > 0 && <h2> You have {unreadMessages.length} unread messages. </h2> } </div> ); } const messages = ['React', 'Re: React', 'Re:Re: React']; ReactDOM.render( <Mailbox unreadMessages={messages} />, document.getElementById('root') );
true && expression은 항상 expression으로 평가되고, false && expression은 항상 false로 평가됩니다.
즉, 조건이 true일때 출력되고, 조건이 false라면 무시됩니다.
조건부 연산자로 If-Else구문 인라인으로 표현하기
조건부 연산자인 condition ? true: false (삼항연산자)를 사용할 수 있습니다.
render() { const isLoggedIn = this.state.isLoggedIn; return ( <div> The user is <b>{isLoggedIn ? 'currently' : 'not'}</b> logged in. </div> ); }
가독성이 좋은 방식을 선택하면 됩니다. 또한 조건이 복잡하다면 컴포넌트를 분리하는 것이 좋습니다.
컴포넌트가 렌더링하는 것을 막기
컴포넌트 자체를 숨기고 싶을때 렌더링 결과를 출력하는 대신 null을 반환하여 해결할 수 있습니다.
리스트와 Key
여러개의 컴포넌트 렌더링하기
엘리먼트 모음을 만들고 중괄호 {}를 이용하여 JSX에 포함시킬 수 있습니다.
map() 함수를 사용하여 number배열을 반복 실행합니다. 각 항목에 대해 <li> 엘리먼트를 반환하고 엘리먼트 배열의 결과를 listItems에 저장합니다.
const numbers = [1, 2, 3, 4, 5]; const listItems = numbers.map((number) => <li>{number}</li> ); ReactDOM.render( <ul>{listItems}</ul>, document.getElementById('root') );
기본 리스트 컴포넌트
일반적으로 컴포넌트 안에서 리스트를 렌더링합니다.
function NumberList(props) { const numbers = props.numbers; const listItems = numbers.map((number) => <li>{number}</li> ); return ( <ul>{listItems}</ul> ); } const numbers = [1, 2, 3, 4, 5]; ReactDOM.render( <NumberList numbers={numbers} />, document.getElementById('root') );
이 코드를 실행하면 리스트의 각 항목에 key를 넣어야 한다는 경고가 표시됩니다. 이때 numbers.map() 안에서 리스트의 각 항목에 key를 할당하여 키 누락 문제를 해결합니다.
function NumberList(props) { const numbers = props.numbers; const listItems = numbers.map((number) => <li key={number.toString()}> {number} </li> ); return ( <ul>{listItems}</ul> ); } const numbers = [1, 2, 3, 4, 5]; ReactDOM.render( <NumberList numbers={numbers} />, document.getElementById('root') );
key
key는 React가 어떤 항목을 변경, 추가 또는 삭제할지 식별하는 것을 돕습니다. key는 엘리먼트에 안정적인 고유성을 부여하기 위해 배얄 내부의 엘리먼트에 지정해야 합니다. key를 선택하는 가장 좋은 방법은 리스트의 다른 항목들 사이에서 해당 항목을 고유하게 식별할 수 있는 문자열을 사용하는 것입니다. 대부분의 경우 데이터의 ID를 key로 사용합니다.
const todoItems = todos.map((todo) => <li key={todo.id}> {todo.text} </li> );
안정적인 ID가 없다면 최후의 순단으로 항목의 인덱스를 key로 사용할 수 있습니다. 그러나 항목의 순서가 바뀔 수 있는 경우 key에 인덱스를 사용하는 것은 권장하지 않습니다.
key로 컴포넌트 추출하기
ListItem컴포넌트를 추출한 경우 ListItem안에 있는 <li>엘리먼트가 아니라 배열의 <LIstItem />엘레먼트가 key를 가져야합니다.
map() 함수 내부에 있는 엘리먼트에 key를 넣어 주는 것이 좋습니다.
function ListItem(props) { // 맞습니다! 여기에는 key를 지정할 필요가 없습니다. return <li>{props.value}</li>; } function NumberList(props) { const numbers = props.numbers; const listItems = numbers.map((number) => // 맞습니다! 배열 안에 key를 지정해야 합니다. <ListItem key={number.toString()} value={number} /> ); return ( <ul> {listItems} </ul> ); } const numbers = [1, 2, 3, 4, 5]; ReactDOM.render( <NumberList numbers={numbers} />, document.getElementById('root') );
key는 형제 사이에서만 고유한 값이어야 한다.
전체범위에서 고유할 필요는 없습니다. 두개의 다른 배열을 만들때 동일한 key를 사용할 수 있습니다.
JSX에 map()포함시키기
중괄호 안에 모든 표현식을 포함 시킬 수 있으므로 map() 함수의 결과를 인라인으로 처리할 수 있습니다.
function NumberList(props) { const numbers = props.numbers; return ( <ul> {numbers.map((number) => <ListItem key={number.toString()} value={number} /> )} </ul> ); }
map()함수가 너무 중첩된다면 컴포넌트로 추출하는 것이 좋습니다.
Forms
HTML 폼 엘리먼트는 Form 엘리먼트 자체가 내부 상태를 가지기 때문에, React의 다른 DOM엘리먼트와 조금 다르게 동작합니다.
<form> <label> Name: <input type="text" name="name" /> </label> <input type="submit" value="Submit" /> </form>
이 Form은 사용자가 form을 제출하면 새로운 페이지로 이동하는 기본 HTML Form 동작을 수행합니다. React에서 동일한 동작을 원한다면 그대로 사용하면 되지만 대부분의 경우, Javascript가 form의 제출을 처리하고 사용자가 입력한 데이터에 접근하도록 하는 것이 편합니다. 이를 위해서 제어 컴포넌트(controlled components)를 이용합니다.
제어 컴포넌트 (Controlled Component)
HTML에서 <input>, <textarea>, <select>는 사용자의 입력을 기반으로 state를 관리하고 업데이트합니다. React에서는 변경할 수 있는 state가 일반적으로 컴포넌트의 state속성에 유지되며 setState()에 의해 업데이트됩니다.
우리는 React state를 '신회 가능한 단일 출처(single source of truth)로 만들어서 두 요소를 결합할 수 있습니다.그러면 폼을 렌더링하는 React 컴포넌트는 폼에 발생하는 사용자 입력갑을 제어합니다. 이러한 방식으로 React에 의해 값이 제어되는 입력 폼 엘리먼트를 '제어 컴포넌트' (controlled component)라고 합니다.
class NameForm extends React.Component { constructor(props) { super(props); this.state = {value: ''}; this.handleChange = this.handleChange.bind(this); this.handleSubmit = this.handleSubmit.bind(this); } handleChange(event) { this.setState({value: event.target.value}); } handleSubmit(event) { alert('A name was submitted: ' + this.state.value); event.preventDefault(); } render() { return ( <form onSubmit={this.handleSubmit}> <label> Name: <input type="text" value={this.state.value} onChange={this.handleChange} /> </label> <input type="submit" value="Submit" /> </form> ); } }
value 어트리뷰트는 폼 엘리먼트에 설정되므로 표시되는 값은 항상 this.state.value가 되고 react state는 신뢰 가능한 단일 출처가 됩니다.
React state를 업데이트하기 위해 모든 키 입력에서 handleChange 가 동작하기 때문에 사용자가 입력할때 보여지는 값이 업데이트됩니다.
제어 컴포넌트로 사용하면 input의 값은 항상 React state에 의해 결정됩니다. 다른 UI 엘리먼트에 input의 값을 전달하거나 다른 이벤트 헨들러에서 값을 재설정할 수 있습니다.
textarea 태그
select 태그
file input 태그
다중입력제어하기
제어되는 input Null 값
제어 컴포넌트에 value prop을 지정하면 의도하지 않는 한 사용자가 변경할 수 없습니다.
제어 컴포넌트의 대한
데이터를 변경할 수 있는 모든 방법에 대해 모든 입력상태를 연결해야 하기 때문에 제어 컴포넌트를 사용하는 게 지루할 수 있습니다. 특히 기존의 코드베이스를 React로 변경하고자 할 때나 React가 아닌 라이브러리와 통합하고자 할 때 짜증날 수 있습니다.
이러한 경우에 입력 폼을 구현하기 위한 재체 기술인 비제어 컴포넌트를 확인할 수 있습니다.
State 끌어올리기
종종 동일한 데이터에 대한 변경사항을 여러 컴포넌트에 반영해야 할 필요가 있습니다. 이럴 대는 가장 가까운 공통 조상으로 state를 끌어올리는 것이 좋습니다.
주어진 온도에서 물의 끓는 여부를 추정하는 온도 계산기를 만들어 봅니다.
BoilingVerdict 컴포넌트는 celsius prop을 받아서 물이 끓기에 온도가 충분한지 출력합니다.
function BoilingVerdict(props) { if (props.celsius >= 100) { return <p>The water would boil.</p>; } return <p>The water would not boil.</p>; }
Calculator 컴포넌트는 온도를 입력할 수 있는 <input>을 렌더링하고 값을 this.state.temperature에 저장합니다.
또한 현재 입력값에 대한 BoilingVerdict 컴포넌트를 렌더링합니다.
class Calculator extends React.Component { constructor(props) { super(props); this.handleChange = this.handleChange.bind(this); this.state = {temperature: ''}; } handleChange(e) { this.setState({temperature: e.target.value}); } render() { const temperature = this.state.temperature; return ( <fieldset> <legend>Enter temperature in Celsius:</legend> <input value={temperature} onChange={this.handleChange} /> <BoilingVerdict celsius={parseFloat(temperature)} /> </fieldset> ); } }
두번째 input 추가하기
화씨 입력필드를 추가하고 두 필드 간에 동기화 상태를 유지합니다.
Calculator에서 TemperatureInput컴포넌트를 빼내고, scale prop을 추가하여 'c'또는 'f'의 값을 가질 수 있도록 합니다.
const scaleNames = { c: 'Celsius', f: 'Fahrenheit' }; class TemperatureInput extends React.Component { constructor(props) { super(props); this.handleChange = this.handleChange.bind(this); this.state = {temperature: ''}; } handleChange(e) { this.setState({temperature: e.target.value}); } render() { const temperature = this.state.temperature; const scale = this.props.scale; return ( <fieldset> <legend>Enter temperature in {scaleNames[scale]}:</legend> <input value={temperature} onChange={this.handleChange} /> </fieldset> ); } }
class Calculator extends React.Component { render() { return ( <div> <TemperatureInput scale="c" /> <TemperatureInput scale="f" /> </div> ); } }
변환함수
한 입력값에 기반하여 다른 나머지 입력값을 계산하는 함수와 올바르지 않는 값에 대해 빈 문자열을 반환하는 함수입니다.
function toCelsius(fahrenheit) { return (fahrenheit - 32) * 5 / 9; } function toFahrenheit(celsius) { return (celsius * 9 / 5) + 32; } function tryConvert(temperature, convert) { const input = parseFloat(temperature); if (Number.isNaN(input)) { return ''; } const output = convert(input); const rounded = Math.round(output * 1000) / 1000; return rounded.toString(); }
State 끌어올리기
현재는 두 TemperatureInput 컴포넌트가 각각의 입력삽을 각자의 state에 독립적으로 저장하고 있습니다.
class TemperatureInput extends React.Component { constructor(props) { super(props); this.handleChange = this.handleChange.bind(this); this.state = {temperature: ''}; } handleChange(e) { this.setState({temperature: e.target.value}); } render() { const temperature = this.state.temperature; // ...
우리는 두 입력값이 서로의 것과 동기화된 상태로 있길 원합니다.
섭씨온도 입력값을 변경할 경우 화씨 온도 입력값 역기 변환된 온도를 반영할 수 있어야 하며, 그 반대의 경우에도 마찬가지입니다.
React에서 state를 공유하는 일은 그 값을 필요로 하는 컴포넌트 간의 가장 가까운 공통 부모로 state를 끌어올림으로써 이뤄낼 수 있습니다. 그러므로 TemperatureInput들이 개별적으로 가지고 있는 state를 지우고 공통부모인 Calculator로 그 값을 옮겨야 합니다.
state가 공통부모인 Calculator로 옮겨가면 두 입력 필드가 서로 일관된 값을 유지하도록 만들기 때문에 진리의 원천(Source of truth)라고 부릅니다. 두 입력필드는 항상 동기화된 상태를 유지할 수 있게 됩니다.
먼저 자식컴포넌트인 TemperatureInput에서 this.state.temperature를 this.props.temperature로 대체합니다.
render() { // Before: const temperature = this.state.temperature; const temperature = this.props.temperature; // ...
props는 읽기 전용이므로 temperature가 부모로부터 prop가 전달되기 때문에 TemperatureInput은 그 값을 제어할 능력이 없습니다.
그러므로 사용자 정의된 TemperatureInput은 temperature와 onTemperatureChange props를 자신의 부모인 Calculator로 부터 건네받습니다.
TemperatureInput에서 온도를 갱신하기 위해 this.props.onTemperatureChange를 호출합니다.
handleChange(e) { // Before: this.setState({temperature: e.target.value}); this.props.onTemperatureChange(e.target.value); // ...
사용자 정의 컴포넌트에서 temperature와 onTemperatureChange prop의 이름이 특별한 의미를 가지고 있지는 않습니다.
onTemperatureChange prop은 부모 컴포넌트인 Calculator로부터 temperature prop과 함께 제공될 것 입니다. 이를 이용해서 자신의 지역 state를 수정하여 변경사항을 처리하므로 변경된 값을 받은 필드는 모두 리렌더링됩니다.
class TemperatureInput extends React.Component { constructor(props) { super(props); this.handleChange = this.handleChange.bind(this); } handleChange(e) { this.props.onTemperatureChange(e.target.value); } render() { const temperature = this.props.temperature; const scale = this.props.scale; return ( <fieldset> <legend>Enter temperature in {scaleNames[scale]}:</legend> <input value={temperature} onChange={this.handleChange} /> </fieldset> ); } }
class Calculator extends React.Component { constructor(props) { super(props); this.handleCelsiusChange = this.handleCelsiusChange.bind(this); this.handleFahrenheitChange = this.handleFahrenheitChange.bind(this); this.state = {temperature: '', scale: 'c'}; } handleCelsiusChange(temperature) { this.setState({scale: 'c', temperature}); } handleFahrenheitChange(temperature) { this.setState({scale: 'f', temperature}); } render() { const scale = this.state.scale; const temperature = this.state.temperature; const celsius = scale === 'f' ? tryConvert(temperature, toCelsius) : temperature; const fahrenheit = scale === 'c' ? tryConvert(temperature, toFahrenheit) : temperature; return ( <div> <TemperatureInput scale="c" temperature={celsius} onTemperatureChange={this.handleCelsiusChange} /> <TemperatureInput scale="f" temperature={fahrenheit} onTemperatureChange={this.handleFahrenheitChange} /> <BoilingVerdict celsius={parseFloat(celsius)} /> </div> ); } }
react 애플리케이션 안에서 변경이 일어나는 데이터에 대해서 진리의 원천을 하나만 두어햐 합니다. 먼저 state를 그 값을 필요로 하는 컴포넌트에 추가한 뒤 같은 값이 필요한 컴포넌트가 존재할 시에 공통조상으로 끌어올리는 것이 좋습니다.
'Today I Learned' 카테고리의 다른 글
TIL_200529 (what is Redux) (0) 2020.05.29 TIL_200528 (0) 2020.05.29 TIL_200525 (What is React) (0) 2020.05.25 TIL_200520 (0) 2020.05.20 TIL_200427-29 (0) 2020.04.30