🥨 리액트 Why? 우리가 CBD 기반의 라이브러리를 직접 사용해보았을 때, 전역상태의 개념 부재, 지역 상태 관리의 어려움 등의 문제를 겪었던 경험이 있다. 리액트는 이러한 어려움을 줄여준다.
👀 props vs state
props는 컴포넌트로 전달된 입력 데이터를 객체화한 것
state는 컴포넌트의 내부적인 상태 데이터를 객체화한 것 ⇒ 컴포넌트 상태 데이터 변경되면 렌더링 발생
비공개 요소이며, 컴포넌트에 의해 완전히 제어된다.
👹 JSX ⇒ 리액트는 컴포넌트라는 느슨하게 연결된 유닛으로 관심사를 분리한다.
React element(객체)를 생성한다. (Babel은 JSX를 React.createElement() 호출로 컴파일한다.)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 const element = ( <h1 className ="greeting" > Hello, world! </h1 > ); const element = React .createElement ( 'h1' , {className : 'greeting' }, 'Hello, world!' ); const element = {type : 'h1' ,props : { className : 'greeting' , children : 'Hello, world!' }
⇒ if, for 문에 사용될 수 있다.
JSX는 HTML보다 JS에 가깝기 때문에 어트리뷰트 이름 대신 camelCase로 명명한다.
ex) class ⇒ className, tabindex ⇒ tabIndex
모든 값을 렌더링하기 전에 이스케이프하므로 악성 사용자가 XSS 공격을 하는 것을 방지할 수 있다.
🖥️ component vs element element DOM element와 달리 일반 객체이다. React DOM은 React 엘리먼트와 일치하도록 DOM을 업데이트 한다. 즉, 가상돔을 구성하고 리얼돔을 이와 일치하도록 한다.
React 엘리먼트를 렌더링하기 위해서 1. ReactDOM.createRoot()에 DOM 엘리먼트 전달 2. React 엘리먼트를 root.render() 에게 전달
1 2 3 4 5 const root = ReactDOM .createRoot ( document .getElementById ('root' ) ); const element = <h1 > Hello, world</h1 > ;root.render (element);
불변객체로, 생성한 이후 해당 엘리먼트의 자식이나 속성을 변경할 수 없다.
특정 시점의 UI를 보여준다고 생각
이를 업데이트 하기 위해서는 새로운 엘리먼트 생성하고 이를 root.render()에게 전달하는 방법뿐
💡 React 엘리먼트를 사용자 정의 컴포넌트로 나타낼 수 있다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 const root = ReactDOM .createRoot (document .getElementById ('root' ));function Welcome (props ) { return <h1 > Hello, {props.name}</h1 > ; } const element = <Welcome name ="Sara" /> ;root.render (element); 혹은 root.render ( <Welcome name ="Sara" /> );
component element들이 모여서 컴포넌트를 이룬다. 엘리먼트는 컴포넌트의 구성요소이다.
컴포넌트를 통해 UI를 재사용 가능한 여러 조각으로 나눔 ⇒ CBD
컴포넌트 정의 방법 ⇒ 함수 생성 (props라는 입력 데이터를 전달받는다)
1 2 3 4 5 6 7 8 9 10 11 function Welcome (props ) { return <h1 > Hello~! Welcome {props.name}</h1 > ; } class Welcome extends React.Component { render ( ) { return <h1 > Hello, {this.props.name}</h1 > ; } }
React 컴포넌트는 자신의 props를 다룰 때, 반드시 순수함수 처럼 동작해야한다.
프론트 개발을 하면서 순수함수로 개발을 한다는 것은 불가능한 일이기에 **state(상태)**가 등장하였다.
🧬 state와 컴포넌트 생명주기 생명주기 메서드를 사용하여 컴포넌트가 마운트되거나 언마운트될 때 일부 코드를 작동할 수 있다.
마운팅 컴포넌트가 처음 DOM에 렌더링 될 때마다 어떤 행동을 하도록 설정해주는 것을 “마운팅”이라고 한다.
언마운팅 컴포넌트가 생성된 DOM에서 삭제될 때마다 어떤 행동을 하도록 설정해주는 것을 “언마운팅”이라고 한다.
🎴 setState 유의사항
this.props, this.state가 비동기적으로 업데이트 될 수 있어 state를 계산할 때, 해당 값에 의존하면 안된다.
1 2 3 4 5 6 7 8 9 this .setState ({ counter : this .state .counter + this .props .increment , }); this .setState ((state, props ) => ({ counter : state.counter + props.increment }));
state가 소유하고 설정한 컴포넌트에만 state를 전달할 수 있다. ⇒ props를 통해 하향식(단방향식)으로만
1 2 3 4 5 function FormattedDate (props ) { return <h2 > It is {props.date.toLocaleTimeString()}.</h2 > ; } <FormattedDate date={this .state .date } />
FormattedDate
컴포넌트는 date
를 자신의 props로 받을 것이고 이것이 Clock
의 state로부터 왔는지, Clock
의 props에서 왔는지, 수동으로 입력한 것인지 알지 못합니다.
💡 모든 state는 항상 특정한 컴포넌트가 소유하고 있으며 그 state로부터 파생된 UI 또는 데이터는 오직 트리구조에서 자신의 “아래”에 있는 컴포넌트에만 영향을 미칩니다.
🌃 이벤트 처리
camelCase 사용
문자열이 아닌 {함수}로 전달
e.preventDefault() 명시적으로 작성
1 2 3 4 5 6 7 8 9 10 11 12 function Form ( ) { function handleSubmit (e ) { e.preventDefault (); console .log ('You clicked submit.' ); } return ( <form onSubmit ={handleSubmit} > <button type ="submit" > Submit</button > </form > ); }
♨️ JSX 콜백안에서 this 바인딩 Javascript 클래스 메서드는 기본적으로 바인딩되어 있지 않는다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 class Toggle extends React.Component { constructor (props ) { super (props); this .state = {isToggleOn : true }; this .handleClick = this .handleClick .bind (this ); } handleClick ( ) { this .setState (prevState => ({ isToggleOn : !prevState.isToggleOn })); } render ( ) { return ( <button onClick ={this.handleClick} > {this.state.isToggleOn ? 'ON' : 'OFF'} </button > ); } }
onClick={this.handleClick}
과 같이 뒤에 ()
를 사용하지 않고 메서드를 참조할 경우, 해당 메서드를 바인딩 해야 합니다.
만약 constructor에서 bind해주기 싫다면 onClick={this.handleClick.bind(this)}
를 해줘야한다.
이벤트 핸들러에 인자 전달 1 2 <button onClick={(e ) => this .deleteRow (id, e)}>Delete Row </button> <button onClick ={this.deleteRow.bind(this, id )}> Delete Row</button >
위줄은 화살표함수, 아랫줄은 bind를 사용하였다.
React 이벤트를 나타내는 e 인자가 ID뒤의 두번째 인자로 전달된다.
bind 사용시 e 인자가 자동으로 전달된다.
🎁 조건부 렌더링
props로 전달되는 조건에 따라 렌더링 해주는 무상태 컴포넌트 렌더링
1 2 3 4 5 6 7 8 9 10 11 12 function Greeting (props ) { const isLoggedIn = props.isLoggedIn ; if (isLoggedIn) { return <UserGreeting /> ; } return <GuestGreeting /> ; } const root = ReactDOM .createRoot (document .getElementById ('root' )); root.render (<Greeting isLoggedIn ={false} /> );
상태를 가지지 않아 함수형 컴포넌트로 생성하였다. props로 전달되는 값에 따라 조건부 렌더링 한다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 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 > ); } } const root = ReactDOM .createRoot (document .getElementById ('root' )); root.render (<LoginControl /> );
컴포넌트가 상태를 가지고 있고 상태를 변경하는 메서드도 지니고 있다. (캡슐화)
상태가 바뀌면 바뀐 상태에 따라 button 엘리먼트의 사용자 정의 컴포넌트가 다르게 할당된다.
상태가 바뀌면 바뀐 상태를 무상태 컴포넌트에게 props로 전달해주고 있다.
💡 너무 조건이 복잡하다면 컴포넌트를 분리하기 좋을 때일 수 있다.
🔑 리스트와 Key 리액트에서 반복문은 주로 map 고차함수를 통해 사용한다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class NumberList extends React.Component { constructor (props ) { super (props); this .state = { numbers : [1 , 2 , 3 , 4 , 5 ], }; } render ( ) { const listItems = this .state .numbers .map ((number ) => ( <li key ={number.toString()} > {number}</li > )); return <ul > {listItems}</ul > ; } }
만약 li에 key props를 주지 않는다면 오류가 발생할 것이다.
Key란? React가 어떤 항목을 식별하기 위한 문자열이다. 주로 ID를 Key로 사용하지만 ID가 없다면 최후의 수단으로 index를 사용한다.
key를 index로 사용하는 것은 권장하지 않는다.
key는 주변 배열의 context에서 의미가 있다. map 함수 내부 엘레먼트에 key 넣자.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 function ListItem (props ) { const value = props.value ; return <li > {value}</li > ; } class NumberList extends React.Component { constructor (props ) { super (props); this .state = { numbers : [1 , 2 , 3 , 4 , 5 , 6 , 7 ], }; } render ( ) { const listItems = this .state .numbers .map ((number ) => ( <ListItem key ={number.toString()} value ={number} /> )); return <ul > {listItems}</ul > ; } }
key는 배열의 형제 요소 사이끼리는 고유해야하지만 두 개의 다른 배열을 만들 때는 동일한 key 사용해도된다.
만약 컴포넌트에서 key와 동일한 값이 필요하다면 이는 key가 아닌 다른 이름의 props로 명시적으로 전달해야한다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 function Blog (props ) { const sidebar = ( <ul > {props.posts.map((post) => <li key ={post.id} > {post.title} </li > )} </ul > ); const content = props.posts .map ((post ) => <div key ={post.id} > <h3 > {post.title}</h3 > <p > {post.content}</p > </div > ); return ( <div > {sidebar} <hr /> {content} </div > ); } const posts = [ {id : 1 , title : 'Hello World' , content : 'Welcome to learning React!' }, {id : 2 , title : 'Installation' , content : 'You can install React from npm.' } ]; const content = posts.map ((post ) => <Post key ={post.id} // X id ={post.id} // O title ={post.title} /> ); const root = ReactDOM .createRoot (document .getElementById ('root' ));root.render (<Blog posts ={posts} /> );
위 예시에서 Post 컴포넌트는 props.id는 읽을 수 있지만, props.key는 읽을 수 없다.
💡 Key는 diff 알고리즘에서 바뀐 부분만 효율적으로 렌더링해주기 위해 필요하다.
React 에서는 사용자의 입력값을 state로 관리하여 React state가 신뢰 가능한 단일 출처(Single Source of Truth)가 된다. 이렇게 React에 의해 값이 제어되는 입력 폼 엘리먼트를 “제어 컴포넌트”라고 한다.
<input type="text">
, <textarea>
및 <select>
모두 매우 비슷하게 동작한다.
textarea HTML에서는 텍스트를 자식노드로 정의하지만, React에서는 textarea 태그에 value 어트리뷰트를 사용한다.
1 <textarea value={this .state .value } onChange={this .handleChange } />
select React에서는 select 태그에서 value 어트리뷰트를 통해 selected를 구현한다.
1 2 3 4 5 6 <select value={this .state .value } onChange={this .handleChange }> <option value ="grapefruit" > Grapefruit</option > <option value ="lime" > Lime</option > <option value ="coconut" > Coconut</option > <option value ="mango" > Mango</option > </select>
multiple옵션 허용 시 value 어트리뷰트에 배열 전달 가능
1 <select multiple={true } value={['B' , 'C' ]}>
📈 State 끌어올리기 보통의 경우 state는 렌더링에 그 값을 필요로 하는 컴포넌트에 먼저 추가한다. 그러고 나서 다른 컴포넌트도 그 값이 필요하게 되면 그 값을 그들의 가장 가까운 공통 조상으로 끌어올리고 공통 조상이 자식 컴포넌트들에게 props를 통해 전달해주면 된다. 이 때, 공통 조상의 state를 변경하는 함수도 props에 담아 같이 전달한다.
하향식 데이터 흐름을 활용
어떤 값이 props 또는 state로부터 계산될 수 있다면, 아마도 그 값을 state에 두어서는 안 됩니다.
ex) celsiusValue
와 fahrenheitValue
를 둘 다 저장하는 대신, 단지 최근에 변경된 temperature
와 scale
만 저장하면 됩니다. 다른 입력 필드의 값은 항상 그 값들에 기반해서 render()
메서드 안에서 계산될 수 있습니다.