🥨  리액트

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!'
);

// JSX는 아래와 같은 element 객체를 생성하고, 이를 통해 화면에 렌더링을 한다.
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()에게 전달하는 방법뿐

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
// Wrong
this.setState({
counter: this.state.counter + this.props.increment,
});

// Correct
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에서 왔는지, 수동으로 입력한 것인지 알지 못합니다.

🌃  이벤트 처리

  • 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`가 작동하려면 아래와 같이 바인딩 해주어야 합니다.
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'));
// Try changing to isLoggedIn={true}:
root.render(<Greeting isLoggedIn={false} />);

상태를 가지지 않아 함수형 컴포넌트로 생성하였다. props로 전달되는 값에 따라 조건부 렌더링 한다.

  • state로 관리하는 유상태 컴포넌트 렌더링
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>; // 여기에 key가 있는 것이 아니다.
}

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) => (
// numbers 배열이 있는 맥락에 key가 있어야 한다.
<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는 읽을 수 없다.

👔  Form Element

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() 메서드 안에서 계산될 수 있습니다.