🤔 태초에 프레임워크가 있었다. MVC(Model-View-Controller)패턴, MVVM(Model-view-ViewModel)패턴, MVW(Model-View-Whatever)패턴 등을 사용했는데 핵심은 모델이었다.
데이터 바인딩을 통해 모델에 있는 값이 변화되면 뷰에서도 이에 맞춰 업데이트 해줘야한다. 즉, 특정 이벤트가 발생했을 때, 모델에 변화를 일으키고, 변화를 일으킴에 따라 어떤 DOM을 가져와서 어떠한 방식으로 뷰를 업데이트 해줄지(Mutation, 변화)가 포인트이다.
🤔 그럼 Mutation을 하지않으면 되는거 아닌가요 ??
매번 이전의 뷰를 지우고 새로운 뷰를 만드는 것은 성능적으로 문제가 되었고 이를 위해 고안한 것이 Virtual DOM.
" We built React to solve one problem: building large applications with data that changes over time. "
우리는 지속해서 데이터가 변화하는 대규모 애플리케이션을 구축하기 위해 React를 만들었습니다.
가상의 DOM.
변화가 일어나면 실제로 브러우저의 DOM에 새로운걸 넣는 것이 아니라, 자바스크립트로 이뤄진 가상 DOM에 한 번 렌더링을 하고, 기존의 DOM과 비교를 한 다음에 변화가 필요한 곳에만 업데이트한다.
예를 들어, 30개의 노드가 수정될 때 30번 뷰를 재렌더링 하는 것은 큰 비용을 요구할 것이다. 따라서 매번 재랜더링 할 게 아니라 비교를 통해서 변화된 곳만 고치도록 고안하였다. 이를 추상화 해 둔 것이 Virtual DOM이다.
이렇게만 보면 성능상의 문제라고 받아들이기 쉬운데, 사실 "리액트"가 가상 DOM을 사용함으로써 성능을 증대시키는 것은 아니다. Virtual DOM은 다른 프론트 엔드 라이브러리에서도 응용하고 있으며 실제로 많은 경우에서 Vue의 성능이 React를 이긴다. 그렇다면 Vue.js는 성능을 최적화해주냐는 질문에는 뭐라고 대답 할 수 있을까? 정답은 "No."이다.
Virtual DOM을 라이브러리들이 제공하는 이유는 개발자에게 이 작업을 스스로 할 필요없이 자동화, 추상화 함으로써 생산성을 증가시키기 위함이다. 사실 개발자가 직접 각각의 변화하는 노드를 비교하는 코드를 짠다면 충분히 어느 라이브러리보다 빠를 수 있다. 이를테면 해당 어플리케이션의 특징과 액션에 맞춰 최적화할 수 있을지도 모른다. 하지만 이는 오버스펙일 수 있으며(과연 그것이 온전히 "어플리케이션" 개발자의 몫인가에 대한 고민) 오히려 생산성이 상당히 저하되는 일을 초래할 수 있다.
또한, DOM관리를 Virtual DOM에게 위임하면 컴포넌트가 DOM 조작 요청을 할 때 다른 컴포넌트들과 상호작용을 하지 않아도 된다. 즉, 특정 DOM을 조작할 것이라던지, 이미 조작했다던지에 대한 정보를 공유할 필요가 없으므로 각 변화들의 동기화 작업을 거치지 않으면서도 모든 작업을 하나로 묶어줄 수 있다.
const element = <h1>Hello, world!</h1>
✏️ JavaScript를 확장한 문법
리액트 엘리먼트를 생성한다.
리액트에서 필수 사용은 아니지만, JavaScript코드 안에서 UI 작업을 할 때 사용한다.
function formatName(user) {
return user.firstName + ' ' + user.lastName;
}
const user = {
firstName: 'Harper',
lastName: 'Perez'
};
const element = (
<h1> Hello, {formatName(user)}! </h1>
);
ReactDOM.render(
element,
document.getElementById('root')
);
JSX의 중괄호 ()
의 attribute에 유효한 모든 JS 표현식을 넣을 수 있으며 따옴표를 이용해 문자열 리터럴을 정의할 수 있다.
❗️NOTE
React DOM은 JSX에 삽입된 모든 값을 렌더링하기 전에 이스케이프하므로 애플리케이션에 작성되지 않은 내용은 주입되지 않는다. 따라서 모든 항목은 렌더링되기 전에 문자열로 변환되므로 XSS 공격이 방지된다.
Babel은 자바스크립트 컴파일러로서, 최신의 자바스크립트 코드를 무난한 이전 단계의 자바스크립트 코드로 변환 가능하게 해주는 개발도구, 즉 트랜스파일러이다.
리액트 컴포넌트들은 대부분 자바스크립트 ES6로 만들어져 있는데, 브라우저들은 리액트 컴포넌트를 이해할 수 없어서 변형이 필요하다. webpack은 직접 변형하지 못하지만 loader의 개념을 가지고 있는데 이게 바로 babel-loader이다.
Babel은 JSX를 React.createElement() 호출로 컴파일한다.
//JSX
const element = (
<h1 className="greeting">
Hello, world!
</h1>
);
//React
const element = React.createElement(
'h1',
{className: 'greeting'},
'Hello, world!'
);
//바로 위의 React 코드가 생성하는 Object
//단순화된 구조임
const element = {
type: 'h1',
props: {
className: 'greeting',
children: 'Hello, world!'
}
};
위의 두 예시(JSX, React)는 동일한 코드이다. JSX를 React문법으로 변환하면 React는 우측처럼 Object를 생성한다.
props
라는 이름의 요소를 가지고 있지만 Hook을 이용하는 함수형 컴포넌트는 그렇지않다.), 화면에 어떻게 표시되는지를 기술하는 React 엘리먼트를 반환한다.//javascript
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}
//ES6
class Welcome extends React.Component {
render() {
return <h1>Hello, {this.props.name}</h1>;
}
}
데이터를 가진 하나의 'props' 객체 인자를 받아 React 엘리먼트로 반환(JSX로 표현돼 있지만 babel에 의해 React.Element로 변환된다.) → 유효한 React 컴포넌트
✏️ JavaScript 함수 꼴이므로 '함수 컴포넌트'라고 부른다.
❗️NOTE
컴포넌트 이름은 항상 대문자로 시작한다. 리액트는 소문자로 시작하는 컴포넌트는 DOM 태그로 처리한다. 예를 들어<div />
는 HTMLdiv 태그
를 나타내지만,<Welcome />
은컴포넌트
를 나타내며, 범위안에 Welcome이 있어야한다.
- 소문자로 시작하는 경우
내장 컴포넌트(e.g.<div>
,<span>
, .. )라는 것을 뜻하며 'div'나 'span'같은 문자열 형태로 React.createElement에 전달된다.- 대문자로 시작하는 경우
React.createElement(Welcome)의 형태로 컴파일되며 JS 파일 내에서 사용자가 정의했거나 import한 컴포넌트를 가리킨다.
`const element = <div />;` //DOM 태그로 나타낸 React 엘리먼트
--------------------------------------------------------
// 사용자 정의 컴포넌트를 이용하여 React 엘리먼트 나타내기
function Welcome(props){ //함수 컴포넌트를 이용한 React 컴포넌트 생성
return <h1>Hello, {props.name}</h1>;
}
const element = <Welcome name="Sara" />; //React 컴포넌트에 React 엘리먼트(name="sara"인 props)적용
ReactDOM.render(
element,
document.getElementById('root')
);
--------------------------------------------------------
// Welcome을 여러번 렌더링하는 App 컴포넌트
function App(){
return (
<div>
<Welcome name="Sara" /> //각각이 엘리먼트이다. 엘리먼트는 컴포넌트의 구성요소
<Welcome name="Cahal" />
<Welcome name="Edite" />
</div>
);
}
ReactDOM.render(
<App />, //App은 props를 따로 주입받지 않는 컴포넌트이다.
document.getElementById('root')
);
위 코드는 다음과 같은 방식으로 동작한다.
1. <Welcome name="Sara" />
엘리먼트로 ReactDOM.render()를 호출
2. React는 {name: 'Sara'}를 props로 하여 Welcome 컴포넌트 호출
3. Welcome 컴포넌트는 <h1>Hello, Sara</h1>
엘리먼트를 반환
4. React DOM은 <h1>Hello, Sara</h1>
엘리먼트와 일치하도록 DOM을 효율적으로 업데이트
🤔 함수꼴로 생긴 클래스라고 생각해보자!!
App에서는 div로 묶인 어떤 '덩어리'를 그대로 리턴한다. 그리고 그 '덩어리' 각각은 name이라는 공통 attribute를 가진 객체이다. 이 객체는 props라고 불리는 데이터이며, Welcome은 해당 데이터의 attribute에 접근하여 이를 다른 형태로 만들어준다.
위 코드에서 setInterval(tick, 100)
이라는 함수를 통해 1초마다 tick()
함수가 수행되고, 강제로 재렌더링을 명시한다. 이를 아래와 같이 캡슐화된 Class를 이용해 변경될 때 마다 이를 감지하여 자동으로 업데이트 하도록 만들 수 있다.
생성자 constructor
render()
생명주기함수
접두사 | 수행 시점 |
---|---|
will | 어떤 작업을 처리하기 직전에 호출 |
did | 어떤 작업을 처리한 후에 호출 |
순서 | 호출 함수 | 라이프 사이클 | 설명 |
---|---|---|---|
1 | constructor | Mount | 컴포넌트를 만들 때 처음으로 호출되며 state의 초기값을 지정할 때 사용 |
2 | getDerivedStateFromProps | Mount | props와 state 값을 동기화할 때 사용하는 함수 (v.16.3+) |
3 | render | Mount | 컴포넌트의 기능과 모양새를 정의하는 함수, 리액트 컴포넌트를 반환 |
4 | componentDidMount | Mount | 컴포넌트를 생성하고 첫 렌더링이 끝났을 때 호출 |
5 | getDerivedStateFromProps | Update | Mount 과정에서도 호출됐던 함수. props와 state 값을 동기화 |
6 | shouldComponentUpdate | Update | 컴포넌트를 리렌더링 할것인지 결정하는 함수. true → 다음 순서의 Update 사이클 내 함수를 모두 호출하여 리렌더링 진행 false → 리렌더링X 다음 순서의 함수 실행X |
7 | render | Update | 새로운 값을 사용하여 View를 리렌더링 |
8 | getSnapshotBeforeUpdate | Update | 변경된 요소에 대하여 DOM 객체에 반영하기 직전에 호출 |
9 | componentDidupdate | Update | 컴포넌트 업데이트 작업이 끝난(리렌더링 완료) 후 호출 |
10 | componentWillUnmount | UnMount | 해당 컴포넌트가 제거되기 직전에 호출 |
setState()를 이용하고, 직접 State를 수정하면 안된다.
State 업데이트는 비동기적일 수 있으며(this.props 또한 마찬가지이다.) 반면에 성능을 위해 setState() 호출을 단일 업데이트로 한꺼번에 처리할 수도 있다.
다음 예시처럼 객체보다 함수를 인자로 사용하는 setState를 사용한다.
State 업데이트는 병합된다.
//다양한 독립적인 변수를 포함한 state
constructor(props){
super(props);
this.state = {
posts: [],
comments: []
};
}
//setState() 호출로 각각의 변수를 독립적으로 업데이트 가능
componentDidMount(){
fetchPosts().then(response => {
this.setState({
posts: response.posts
});
});
fetchComments().then(response => {
this.setState({
comments: response.comments
});
});
}
병합은 얕게 이루어지므로, this.setState({comments})는 this.state.posts에 영향을 주진 않지만 this.state.comments는 완전히 대체된다.
✏️ 데이터는 아래로 흐른다.
state가 소유하고 설정한 컴포넌트 이외에는 어떠한 컴포넌트에도 접근할 수 없다. 모든 컴포넌트는 완전히 독립적이며, 모든 state는 항상 특정한 컴포넌트가 소유하고 그 state로부터 파생된 UI 또는 데이터는 오직 트리구조에서 자신의 '아래'에 있는 컴포넌트에만 영향을 미친다.
컴포넌트는 자신의 state를 자식 컴포넌트에 props로 전달할 수 있다.
class Clock extends React.Component{
...
render(){
return (
<div>
<h1>Hello, world!</h1>
<FormattedDate date={this.state.date} />
</div>
);
}
}
//FormattedDate 컴포넌트는 date를 자신의 props로 받을 것이다.
function FormattedDate(props){
return <h2>It is {props.date.toLocaleTimeString()}.</h2>;
}
props (properties)란 사용자가 컴포넌트에 전달해서 보관하길 원하는 데이터이다. 컴포넌트 내에서 데이터가 보관되면, 이 데이터는 수정되지 않고 보존되어야 하는 법칙이 있다. 따라서 이는 읽기 전용이며 만약 props 값을 변경하고자 하면 부모 컴포넌트에서 이에 대한 부분이 변경되어야한다.
React 컴포넌트는 컴포넌트의 상태(state)를 저장할 수 있으며 state는 해당 컴포넌트 내부에 존재하므로 상태값 변경이 가능하다.
부모 객체는 자식객체에 props 값을 전달하며, props 값을 받은 자식객체는 이에 관한 부분들을 렌더링 하며, state라는 자체 값을 포함하여 데이터를 변경해 주고, 다시 렌더링 해줄 수 있다.
ref.https://overreacted.io/ko/how-are-function-components-different-from-classes/
React 엘리먼트에서 이벤트를 처리하는 방식은 DOM 엘리먼트에서 이벤트를 처리하는 방식과 매우 유사하다.
몇 가지 문법 차이는 다음과 같다.
//in HTML
<button onclick="activateLasers()">
Activate Lasers
</button>
//in React
<button onClick={activateLasers}>
Activate Lasers
</button>
//in HTML
<a href="#" onclick="console.log('The link was clicked'); return false">
Click me
</a>
//in React
function ActionLink(){
function handleClick(e) {
e.preventDefault();
console.log('The link was clicked');
}
return (
<a href="#" onClick={handleClick}>
Click me
</a>
)
}
ReactDOM.render(
<ActionLink />,
document.getElementById('root');
);
e ← 합성 이벤트 (ref. https://ko.reactjs.org/docs/events.html)
DOM 엘리먼트가 생성된 후 addEventListener를 호출하여 이벤트 리스너를 붙일 필요 없다. (엘리먼트가 처음 렌더링될 때 리스너를 제공한다.)
❗️NOTE
EventListener : 해당 이벤트에 대해 대기하다가 등록한 이벤트가 수행될 시 특정 함수를 수행한다.
document.getElementById('myComponent').addEventListener('click', onClick);
document.getElementById('myComponent').addEventListener('click', onClick2);
// 클릭에 대해서 2개의 이벤트가 붙여짐
document.getElementById('myComponent').removeEventListener('click', onClick);
// 처음 추가한 이벤트 제거함
다음은 이벤트 핸들러를 클래스의 메서드로 만들어서 적용한 코드이다.
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')
);
원하는 동작을 캡슐화하는 컴포넌트를 만들어 애플리케이션의 상태에 따라 컴포넌트 중 몇 개만을 렌더링 할 수 있다.
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 (isLoggendIn){ // if문을 사용한 조건부 렌더링
return <UserGreeting />;
}
return <GuestGreeting />;
}
ReactDOM.render(
<Greeting isLoggedIn={false} />, //<h1>Please sign up.</h1>
document.getElementById('root')
)
function Mailbox(props) {
const unreadMessages = props.unreadMessages;
return {
<div>
<h1>Hello!</h1>
{unreadMessages.length > 0 &&
<h2>You have {unreadMessages.length} unread msgs.</h2>
} // unreadMessages.length > 0 가 false이면 false가 하나만 있어도 결과값이 false로 단정되므로 뒷부분은 실행되지 않고 넘어간다.
</div>
}
}
const messages = ['React', 'Re: React', 'Re:Re: React'];
ReactDOM.render(
<Mailbox unreadMessage={message} />,
document.getElementById('root')
)
true && expression → expession
false && expression → false
function Mailbox(props) {
return {
<div>
the user is <b>{props.isLoggedIn ? 'currently' : 'not'}</b> logged in.
</div>
}
}
ReactDOM.render(
<Mailbox isLoggedIn={true} />,
document.getElementById('root')
)
--------------------------
function Mailbox(props) {
return {
<div>
{props.isLoggedIn ? <LogoutButton onClick={logoutClick} /> : <LoginButton onClick={loginClick} />}
</div>
}
}
function LogoutButton(props){
function logoutClick(){
// action
}
return {
<button>Logout</button>
}
}
function LoginButton(props){
function loginClick(){
// action
}
return {
<button>Login</button>
}
}
ReactDOM.render(
<Mailbox isLoggedIn={true} />,
document.getElementById('root')
)
다음은 map() 함수를 사용해 numbers 배열의 값을 두배로 만든 후 map()에서 반환하는 새 배열을 doubled 변수에 할당하고 로그를 확인하는 코드이다.
const numbers = [1,2,3,4,5];
const doubled = numbers.map((number) => number*2)
console.log(doubled);
React에서 배열을 엘리먼트로 만드는 방식은 이와 거의 동일하다.
다음처럼 엘리먼트 모음을 만들고 중괄호{}를 이용하여 JSX에 포함시킬 수 있다.
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 key={number.toString()}>{number}</li>
);
return (
<ul>{listItems}</ul>
);
}
const numbers = [1,2,3,4,5];
ReactDOM.render(
<NumberList numbers={numbers} />,
document.getElementById('root')
);
React가 어떤 항목을 변경, 추가 또는 삭제할지 식별하는 것을 돕는다. 엘리먼트에 안정적인 고유성을 부여하기 위해 배열 내부의 엘리먼트에 지정해야한다. 즉, 리스트의 다른 항목들 사이에서 고유하게 식별 가능한 문자열을 사용해야하는데 보통 데이터의 id를 이용한다.
키는 주변 배열의 context에서만 의미가 있다.
e.g. ListItem 컴포넌트를 추출한 경우, ListItem 안의 <li>
엘리먼트가 아니라 배열의 <ListItem />
엘리먼트가 Key를 가져야한다.
function ListItem(props){
return <li>{props.value}</li>; //여기는 key 지정할 필요없음
}
function NumberList(props){
const numbers = props.numbers;
const listItems = numbers.map((number) =>
<ListItem key={number.toString()} value={number} />
// ListItem 컴포넌트 자체가 key를 가져야한다.
);
return (
<ul>
{listItems}
</ul>
);
}
const numbers = [1,2,3,4,5];
ReactDOM.render(
<NumberList numbers={numbers} />
document.getElementById('root')
);
----------------------------------
//JSX에 map() 포함시키기
function NumberList(props) {
const numbers = props.numbers;
return (
<ul>
{numbers.map((number) =>
<ListItem key={number.toString()}
value={number} />
)}
</ul>
);
}
Key는 배열의 형제 사이에서만 고유한 값이면 되며 전체 전체 범위에서 고유할 필요는 없다. 따라서 두 개의 다른 배열을 만들 때 동일한 key를 사용할 수 있다. Key는 힌트를 제공할 뿐, 컴포넌트로 전달되지 않는다.⭐️
컴포넌트에서 key와 동일한 값이 필요하다면 다른 이름의 prop으로 명시적으로 전달한다.
리액트에서 기본 HTML 폼 동작을 동일하게 수행하길 바란다면 그대로 사용하면되지만 대부분의 경우, JS 함수로 폼의 제출을 처리하고 사용자가 폼에 입력한 데이터에 접근하도록 하는 것이 편리하다. 이를 위해 표준 방식은 '제어 컴포넌트'라고 불리는 기술을 이용한다.
HTML에서 <input>
, <textarea>
, <select>
와 같은 폼엘리먼트는 일반적으로 사용자의 입력을 기반으로 자신의 state를 관리하고 업데이트한다. React에서는 변경할 수 있는 state가 일반적으로 컴포넌트의 state 속성에 유지되며, setState()에 의해 업데이트된다.
React는 state를 신뢰가능한 단일 소스로 만들어 두 요소를 결합한다. 폼을 렌더링하는 React 컴포넌트는 폼에 발생하는 사용자 입력값을 제어하는데, 이때 제어 당하는 입력폼 엘리먼트가 제어 컴포넌트이다.
함수 컴포넌트에서 수행하는 예제이다. :
export default function Example ({ version, setVersion }){
...
const handleChangeVersion = (event) => {
setVersionName(event.target.value)
}
...
return {
<TextField id="input-field" value={version} onChange={handleChangeVersion} />
}
}
✔️TextField
는 input type='text'
와 동일하다.
<input type="text">
, <textarea>
, <select>
모두 매우 비슷하게 동작한다.