React 공식문서 주요개념 읽기 - 1
개요
- 리액트를 다루기에 앞서 리액트 공식문서를 읽어보고 정리하며 공식문서를 읽는 습관, 리액트의 기본적인 개념과 문법을 익히는 시간을 가지고자 한다.
- 💁♂️ 로 표시한 내용은 글을 읽고난 후 저의 생각정리이며, 나머지는 공식문서의 내용과 일치합니다.
JSX 소개
JSX란?
const element = <h1>Hello, world!</h1>;
위 태그 문법은 string, HTML 둘 다 아니다. JSX라 하며 자바스크립트를 확장한 문법이다. JSX는 React element를 생성한다.
React는 별도의 파일에 마크업과 로직을 넣어 기술을 분리하는 대신, 둘 다 포함하는 "Component"라고 부르는 느슨하게 연결된 유닛으로 관심사를 분리한다. 즉 기존에는 .html 파일과, .js 파일을 분리하여 연결해서 사용했지만 컴포넌트를 사용하면 분리하지 않고 한 번에 작성이 가능하다는 것을 의미한다.
💁♂️ JavaScript Extension의 줄임말인 JSX를 통하여 HTML 태그를 변수 안에 넣어서 사용할 수 있게 되었으니 .html파일을 굳이 생성하지 않고 .js 파일 내에서 모두 해결이 가능하도록 해주는 편리한 라이브러리라고 생각든다.
JSX에 표현식 포함하기
const name = 'Olaf'; // name 변수 선언
const element = <h1>Hello, {name}</h1>; // name을 중괄호로 감싸 JSX에 사용
ReactDOM.render(element, document.getElementById('root'));
💁♂️ JSX 중괄호 내에는 모든 JavaScript 표현식을 넣을 수 있다. JSX에서 자바스크립트 문법을 사용하려면 무조건 { } 로 감싸야 한다. 위에서는 const로 선언한 name 변수에 있는 값을 가져오기 위해서 중괄호로 감싼 {name}을 사용했다. 결과적으로 Hello, Olaf가 출력될 것이다.
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')
);
작업을 수행할 세미콜론 자동 삽입 기능을 피하고자 괄호로 묶는 것을 권장한다.
💁♂️ 자바스크립트는 자바스크립트 엔진이 소스코드를 해석할 때 문의 끝이라고 예측되는 지점에 세미콜론을 자동으로 붙여주는 세미콜론 자동 삽입 기능(ASI; automatic semicolon insertion)이 수행된다고 한다. 이 때 세미콜론 자동 삽입 기능의 동작과 개발자의 예측이 일치하지 않는 경우가 간혹 있다고 한다. 자바스크립트 또한 세미콜론을 찍는가, 안 찍는가에 대해서 의견이 분분한데 React 또한 '권장'이라는 말을 쓰는 것을 보아하니 자바스크립트와 똑같은 맥락으로 보인다.
JSX도 표현식이다
컴파일이 끝나면, JSX 표현식이 정규 JavaScript 함수 호출이 되고 JavaScript 객체로 인식된다. JSX를 if 구문 및 for loop 안에 사용하고, 변수에 할당하고, 인자로서 받아들이고, 함수로부터 반환할 수 있다.
function getGreeting(user) {
if (user) {
return <h1>Hello, {formatName(user)}!</h1>;
}
return <h1>Hello, Stranger.</h1>;
}
JSX 속성 정의
// attribute에 따옴표를 이용해 문자열 리터럴 정의
const element = <a href="https://www.reactjs.org">link</a>;
// attribute에 중괄호를 사용해 JavaScript 표현식 삽입
const element = <img src={user.avatarUrl}></img>;
// 💁♂️ 마찬가지로 자바스크립트 문법으로 변수에 담겨져 있는 값을 가져오기 위해 { }를 사용했다.
JSX는 JavaScript에 가깝기 때문에 React DOM은 HTML attribute 이름 대신 camelCase 프로퍼티 명명 규칙을 사용한다. JSX에서 class는 className가 된다.
JSX로 자식 정의
// 태그가 비어있다면 />를 이용해 바로 닫아주어야 한다.
const element = <img src={user.avatarUrl} />;
// 💁♂️ React에서는 모든 태그에서 />이 사용 가능하다고 한다. 심지어 <div>도!
// JSX 태그는 자식을 포함할 수 있다.
const element = (
<div>
<h1>Hello!</h1>
<h2>Good to see you here.</h2>
</div>
);
💁♂️ 여러 태그를 포함한다면 소괄호 '('로 묶는 것을 확인!
JSX는 객체를 표현한다.
Babel은 JSX를 React.createElement() 호출로 컴파일한다.
// 1
const element = (
<h1 className="greeting">
Hello, world!
</h1>
);
// 2
const element = React.createElement(
'h1',
{className: 'greeting'},
'Hello, world!'
);
// 3
const element = {
type: 'h1',
props: {
className: 'greeting',
children: 'Hello, world!'
}
};
1번과 2번은 동일한 예시이다. 2번 코드의 React.createElement() 는 버그가 없는 코드를 작성하는 데 도움이 되도록 몇 가지 검사를 수행해서 3번 코드와 같은 객체를 생성한다. 3번과 같은 객체를 React 엘리먼트라고 하며, 화면에서 보고 싶은 것을 나타내는 표현이라고 한다.
💁♂️ 이 부분에 대해서 이해가 어려웠다. 1번 코드를 입력하면 Babel 이라는 것이 2번과 같은 코드로 컴파일 한다는 의미 같은데 Babel이 무엇인지 살짝 알아보았다. Babel은 최신 문법을 써도 이전 브라우저 환경에서 사용할 수 있도록 최신 문법을 예전버전의 js로 변환해주는 툴이라고 한다. 우리가 1번 코드처럼 HTML태그로 편하게 작성하면 Babel이 알아서 화면에 렌더해주는 것을 의미하는게 아닐까? React가 무엇인지 조사하면서 리액트의 특징 중 하나가 선언적 개발이었는데 이 부분이 아닌가 싶다.
엘리먼트 렌더링
엘리먼트는 React app의 가장 작은 단위이다. 엘리먼트는 화면에 표시할 내용을 기술한다.
const element = <h1>Hello, world</h1>;
DOM에 엘리먼트 렌더링하기
HTML 파일 어딘가에 <div id="root"></div>가 있다고 가정해 본다. 이 안에 들어가는 모든 엘리먼트를 React DOM에서 관리하기 때문에 이것을 root DOM 노드라고 부른다. React로 구현된 애플리케이션은 일반적으로 하나의 루트 DOM 노드가 있다. React 엘리먼트를 루트 DOM 노드에 렌더링하려면 둘 다 ReactDOM.render() 로 전달하면 된다.
const element = <h1>Hello, world</h1>;
ReactDOM.render(element, document.getElementById('root'));
변경된 부분만 업데이트하기
React DOM은 해당 엘리먼트와 그 자식 엘리먼트를 이전의 엘리먼트와 비교하고 DOM을 원하는 상태로 만드는데 필요한 경우에만 DOM을 업데이트한다. 매초 전체 UI를 다시 그리도록 엘리먼트를 만들었지만 React DOM은 내용이 변경된 텍스트 노드만 업데이트했다.
Components와 Props
개념적으로 컴포넌트는 JavaScript 함수와 유사하다. props라고 하는 임의의 입력을 받은 후, 화면에 어떻게 표시되는지를 기술하는 React 엘리먼트를 반환한다.
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}
데이터를 가진 하나의 props 객체 인자를 받은 후 React 엘리먼트를 반환하므로 유효한 React 컴포넌트이다. 이 컴포넌트는 JavaScript 함수이기 때문에 함수 컴포넌트라고 호칭한다.
class Welcome extends React.Component {
render() {
return <h1>Hello, {this.props.name}</h1>;
}
}
ES6 class를 사용하여 컴포넌트를 정의할 수 있고 클래스 컴포넌트라고 호칭한다.
💁♂️ React 16.8 버전에서 hooks라는 기능이 추가되면서 함수형을 많이 사용한다고 한다. 추가로 생활코딩에서 React 영상을 보고 첨언하자면 클래스 컴포넌트에는 extends React.Component, render() {}를 필히 붙여야 한다. 현재는 함수형을 많이 사용하지만 클래스형을 해석할 수는 있어야 한다.
컴포넌트 렌더링
const element = <Welcome name="Sara" />;
React 엘리먼트는 사용자 정의 컴포넌트로도 나타낼 수 있다. React가 사용자 정의 컴포넌트로 작성한 엘리먼트를 발견하면 JSX 어트리뷰트와 자식을 해당 컴포넌트에 단일 객체로 전달한다. 이 객체를 props라고 한다.
// 3. Welcome 컴포넌트는 <h1>Hello, Sara</h1> 엘리먼트를 반환한다.
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}
// 2. {name= 'Sara'}를 props로 하여 Welcome 컴포넌트를 호출한다.
const element = <Welcome name="Sara" />;
// 1. <Welcome name="Sara" /> 엘리먼트로 ReactDOM.render()를 호출한다.
ReactDOM.render(element, document.getElementById('root'));
// 4. <h1>Heelo, Sara</h1> 엘리먼트와 일치하도록 DOM을 효율적으로 업데이트한다.
💁♂️ 컴포넌트의 이름은 항상 대문자로 시작해야 한다.
컴포넌트 합성
컴포넌트는 자신의 출력에 다른 컴포넌트를 참조할 수 있다. 모든 세부 단계에서 동일한 추상 컴포넌트를 사용할 수 있음을 의미한다. React 앱에서는 버튼, 폼, 다이얼로그, 화면 등의 모든 것들이 흔히 컴포넌트로 표현된다. 예를 들어서 Welcome을 여러 번 렌더링하는 App 컴포넌트를 만들 수 있다.
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}
function App() {
return (
<div>
<Welcome name="Sara" />
<Welcome name="Cahal" />
<Welcome name="Edite" />
</div>
);
}
ReactDOM.render(<App />, document.getElementById('root'));
💁♂️ HTML 태그로 인스타그램을 만들 때 비슷한 양식의 리스트를 만드는데 똑같은 태그를 반복하여 만드는 점이 불편했었다. 동일한 컴포넌트 재사용이 가능하다면 반복적으로 화면을 구성해야할 부분에 대해서 매우 편리할 것으로 생각든다.
컴포넌트 추출
"컴포넌트를 여러 개의 작은 컴포넌트로 나누는 것을 두려워하지 마세요."
function Comment(props) {
return (
<div className="Comment">
<div className="UserInfo">
<img className="Avatar"
src={props.author.avatarUrl}
alt={props.author.name}
/>
<div className="UserInfo-name">
{props.author.name}
</div>
</div>
<div className="Comment-text">
{props.text}
</div>
<div className="Comment-date">
{formatDate(props.date)}
</div>
</div>
);
}
Comment 컴포넌트는 author(객체), text(문자열) 및 date(날짜)를 props로 받은 후 소셜 미디어 웹 사이트의 코멘트를 나타낸다.
이 컴포넌트 구성요소들이 모두 중첩 구조로 이루어져 있어서 변경하기 어려울 수 있으며, 각 구성요소를 개별적으로 재사용하기도 힘들다. 이 컴포넌트에서 몇 가지 컴포넌트를 추출할 것이다.
먼저 Avatar를 추출한다.
function Avatar(props) {
return (
<img className="Avatar"
src={props.user.avatarUrl}
alt={props.user.name}
/>
);
}
Avatar는 자신이 Comment 내에서 렌더링 된다는 것을 알 필요가 없다. 따라서 props의 이름을 author에서 더욱 일반화된 user로 변경하였다. props의 이름은 사용될 context가 아닌 컴포넌트 자체 관점에서 짓는 것을 권장한다.
이제 Comment가 살짝 단순해졌다.
function Comment(props) {
return (
<div className="Comment">
<div className="UserInfo">
<Avatar user={props.author} />
<div className="UserInfo-name">
{props.author.name}
</div>
</div>
<div className="Comment-text">
{props.text}
</div>
<div className="Comment-date">
{formatDate(props.date)}
</div>
</div>
);
}
다음은 Avatar 옆에 사용자의 이름을 렌더링하는 UserInfo 컴포넌트를 추출하겠다.
function UserInfo(props) {
return (
<div className="UserInfo">
<Avatar user={props.user} />
<div className="UserInfo-name">
{props.user.name}
</div>
</div>
);
}
Comment가 더욱 단순해졌다.
function Comment(props) {
return (
<div className="Comment">
<UserInfo user={props.author} />
<div className="Comment-text">
{props.text}
</div>
<div className="Comment-date">
{formatDate(props.date)}
</div>
</div>
);
}
props는 읽기 전용이다
함수 컴포넌트나 클래스 컴포넌트 모두 컴포넌트 자체 props를 수정해서는 안된다.
function sum(a, b) {
return a + b;
}
이런 함수들은 순수 함수라고 호칭한다. 입력값을 바꾸려 하지 않고 항상 동일한 입력값에 대해 동일한 결과를 반환하기 때문이다. 반면에 다음 함수는 자신의 입력값을 변경하기 때문에 순수 함수가 아니다.
function withdraw(account, amount) {
account.total -= amount; // account.total = account.total - amount;
}
/*
💁♂️ 위 함수가 순수 함수가 아닌 이유? sum 함수 에서는 언제 어디서라도 a, b 값을 넣으면 항상 똑같은 결과를 출력한다.
반면 withdraw 함수의 경우 함수 내에서 외부의 account.total 값이 변하면 결과도 달라지기 때문이다.
*/
React는 매우 유연하지만 한 가지 엄격한 규칙이 있는데 모든 React 컴포넌트는 자신의 props를 다룰 때 반드시 순수 함수처럼 동작해야 한다.
💁♂️ 리액트 공식문서 주요개념 1~4를 보면서 리액트가 무엇인지 어느정도 이해할 수 있었다. 계속해서 남은 주요개념 5~12를 정리하면서 리액트에 대해 전반적으로 이해할 계획이다.