[React] 공식문서 학습(문서) : 주요 개념
🧐 서론
문서의 첫 시작이었던 설치는 아무래도 개발보다는 전반적인 소개에 가까웠다.
이 주요개념을 공부하는 것이 React를 시작하며 허술했던 기반을 다져줄 수 있을거라 기대된다! (분량이 많다면 나누어야할듯)
💙 설치
1. Hello World
ReactDOM.render(
<h1>Hello, world!</h1>,
document.getElementById('root')
);
모든 언어와 프레임워크를 시작하면 "Hello World" 출력이 첫 걸음이다.
이 역시, React의 가장 기본적인 조작으로 "Hello, world!" 글귀의 제목 엘리먼트(혹은 요소, 태그 등) 를 렌더링한 모습이다.
2. JSX 소개
const element = <h1>Hello, world!</h1>;
React 컴포넌트가 반환해야 할 형태이며, 이는 HTML도, String 도 아니다.
말 그대로, JSX(Javascript eXtension) 확장문법이므로, 자바스크립트에 해당하며 이는 React 엘리먼트를 생성하는 문법이다.
- JSX 란?
위에서 언급했듯 Javascript의 확장문법이되, HTML 형태처럼 작성된다.
React는 이벤트 처리 방식, state 변화, 데이터 준비 등 렌더링 로직들이 UI 로직과 연결된다. (이에 따른 리렌더링이 발생하겠지?)
JSX는 내부에 Javascript 문법을 작성할 수 있는 만큼 UI와 로직작업을 같이 하기에 시각적으로 좋으며, React 역시 JSX에 최적화되있다.
- JSX의 문법 및 사용방법
- 중괄호({})를 통해 JSX 내에서 Javascript 표현식을 활용할 수 있다.
- JSX 역시 Javascript로, 컴파일 시 Javascript 객체로 변환된다. 그렇기에, JSX 자체도 표현식이므로 반복문, 조건문, 변수에 사용가능
- JSX 속성은 문자열, JS 표현식 등으로 하달 가능하다.(동시사용 불가) 또, JSX는 Javascript 이므로 camelCase 권장
- JSX의 class는 JS Class 문법과 혼선을 피하기 위해 className 으로 하달한다. (id는 동일)
- JSX은 XML 처럼 셀프 클로징( />)을 무조건 해주어야 한다.(div 같은 페어태그도 셀프 클로징 가능)
- JSX 태그 내 자식 엘리먼트들을 포함할 수 있다. 컴포넌트의 반환값으로서 JSX는 반드시 하나의 큰 부모태그로 랩핑되어야 한다.
- JSX는 객체 형태로 Babel 에서 컴파일된다. React.createElement() 호출을 통해 검사를 수행한 뒤 객체를 생성한다.
// JSX
const element = (
<h1 className="greeting">
Hello, world!
</h1>
);
// to Object
// 주의: 다음 구조는 단순화되었습니다
const element = {
type: 'h1',
props: {
className: 'greeting',
children: 'Hello, world!'
}
};
이 객체가 React 엘리먼트이며, 화면에 보고싶은 것을 표현하는 요소이다. React는 이 객체를 읽어 DOM을 구성하고 최신상태로 유지한다.
3. 엘리먼트 렌더링
- React 엘리먼트
화면에 표시되는 요소. 이는 일반객체이자 불변객체이며, 엘리먼트는 컴포넌트의 구성요소이다.
const element = <h1>Hello, world</h1>;
ReactDOM.render(element, document.getElementById('root'));
이 엘리먼트들은 React DOM의 루트(root) DOM 노드에 포함되며, 이를 통해 렌더링되는 것이다.
React DOM은 엘리먼트(및 자식 엘리먼트들)을 이전 엘리먼트와 비교하여 필요한 경우에 DOM을 업데이트하는 것이다. (필요한 부분만)
4. Component 와 Props
- 컴포넌트
UI를 재사용 가능한 개별적인 여러 조각으로 나눈 단위를 컴포넌트라고 한다.
Javascript 함수와 유사하며, props 를 입력받아 화면에 표시할 React 엘리먼트를 반환한다.
- 클래스 컴포넌트와 함수 컴포넌트
// Function Component
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}
// Class Component
class Welcome extends React.Component {
render() {
return <h1>Hello, {this.props.name}</h1>;
}
}
공식문서도 현재는 필요한 경우 외에는 함수 컴포넌트 사용을 권장한다. (간결성)
* 함수형 컴포넌트 vs 함수 컴포넌트
이 역시 면접에서 받았던 흥미로운 질문이었다.
함수형 프로그래밍은 순수 함수를 다루기에, 같은 입력값(여기선 props)마다 항상 같은 출력값을 보장해야 한다.
하지만, React는 state 변화, 메서드에 따른 결과값 변경이 실시간으로 일어날 수 있으며, 메서드나 로직에 따른 Side Effect가 발생한다.
그렇기에, 공식문서도 확실하게 함수(Function) 컴포넌트라 칭하며, 함수의 형태를 가지나 함수형 패러다임에는 불합하는 점을 명확히 한다.
* 참고 포스팅 : gyuwon.github.io/blog/2020/07/24/react-has-no-functional-components.html
- 컴포넌트 관련 내용
- 컴포넌트 렌더링 : render() 함수 -> props 전달 -> JSX 반환 -> DOM 업데이트 순. 컴포넌트는 무조건 대문자로 시작.
- 컴포넌트 합성 : 자신의 출력에 다른 컴포넌트를 참조. App이 다른 컴포넌트를 포괄하듯, 컴포넌트는 하위 컴포넌트를 가질 수 있음.
- 컴포넌트 추출 : 다단계층의 엘리먼트를 각각의 컴포넌트로 나눔. 반복적인 UI 혹은 자체적으로 복잡한 컴포넌트에 반영하기를 권장.
- Props
부모 컴포넌트에선 자식 컴포넌트로 속성처럼 props를 하달, 자식 컴포넌트(함수)의 매개변수로 props가 들어온다.
React의 한 가지 엄격한 규칙으로, 모든 컴포넌트는 자신의 props를 다룰 때 반드시 순수 함수처럼 동작해야 한다. (읽기 전용, 수정 X)
5. State와 Lifecycle
- State
state는 컴포넌트의 일종의 "상태값"이며, 컴포넌트에 캡슐화(비공개) 되어있다. state는 주로 클래스형 컴포넌트에서 사용되어온 기능이다.
class Clock extends React.Component {
constructor(props) {
super(props);
this.state = {date: new Date()};
}
render() {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {this.state.date.toLocaleTimeString()}.</h2>
</div>
);
}
}
ReactDOM.render(
<Clock />,
document.getElementById('root')
);
공식문서의 <Clock /> 이라는 타이머 컴포넌트이다. constructor() 메서드를 통해 state를 설정하고 props를 불러온다.
- state는 this.state에 접근하여 수정하는 것이 아니다.(리렌더링 되지 않음) setState() 세터 함수를 통해서만 수정한다.
- state 업데이트는 비동기적이다. 그렇기에, setState에 state 객체가 아닌, 콜백함수(이전 state를 인자로)를 통해 오류를 방지한다.
- state 업데이트는 병합된다. setState()는 내부 객체를 현재 state에 병합하므로 한 프로퍼티가 다른 프로퍼티에 영향을 주지 않음.
state는 특정 컴포넌트에서 갖는 상태값이며 다른 컴포넌트에서 접근할 수 없다. 그렇기에, 하위 컴포넌트론 props 형태로 하달한다.
- Lifecycle
React에서 컴포넌트가 DOM에 렌더링되는 것을 "마운팅" 이라고 한다. (반대로, DOM에서 삭제되는것은 "언마운팅")
이러한 컴포넌트의 각 시점에 특정코드를 사용할 수 있도록 React에서 지원하는 메서드를 "Lifecycle Method" 라고 명명하는 것이다.
class Clock extends React.Component {
constructor(props) {
super(props);
this.state = {date: new Date()};
}
componentDidMount() {
this.timerID = setInterval(
() => this.tick(),
1000
);
}
componentWillUnmount() {
clearInterval(this.timerID);
}
tick() {
this.setState({
date: new Date()
});
}
render() {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {this.state.date.toLocaleTimeString()}.</h2>
</div>
);
}
}
ReactDOM.render(
<Clock />,
document.getElementById('root')
);
- <Clock />가 ReactDOM.render()로 전달되었을 때 React는 Clock 컴포넌트의 constructor를 호출합니다. Clock이 현재 시각을 표시해야 하기 때문에 현재 시각이 포함된 객체로 this.state를 초기화합니다. 나중에 이 state를 업데이트할 것입니다.
- React는 Clock 컴포넌트의 render() 메서드를 호출합니다. 이를 통해 React는 화면에 표시되어야 할 내용을 알게 됩니다. 그 다음 React는 Clock의 렌더링 출력값을 일치시키기 위해 DOM을 업데이트합니다.
- Clock 출력값이 DOM에 삽입되면, React는 componentDidMount() 생명주기 메서드를 호출합니다. 그 안에서 Clock 컴포넌트는 매초 컴포넌트의 tick() 메서드를 호출하기 위한 타이머를 설정하도록 브라우저에 요청합니다.
- 매초 브라우저가 tick() 메서드를 호출합니다. 그 안에서 Clock 컴포넌트는 setState()에 현재 시각을 포함하는 객체를 호출하면서 UI 업데이트를 진행합니다. setState() 호출 덕분에 React는 state가 변경된 것을 인지하고 화면에 표시될 내용을 알아내기 위해 render() 메서드를 다시 호출합니다. 이 때 render() 메서드 안의 this.state.date가 달라지고 렌더링 출력값은 업데이트된 시각을 포함합니다. React는 이에 따라 DOM을 업데이트합니다.
- Clock 컴포넌트가 DOM으로부터 한 번이라도 삭제된 적이 있다면 React는 타이머를 멈추기 위해 componentWillUnmount() 생명주기 메서드를 호출합니다.
6. 이벤트 처리하기
- 소문자 대신 camelCase 를 사용한다. (onclick => onClick)
- JSX를 사용하여 문자열이 아닌 함수로 이벤트 핸들러를 전달한다. (onClick={handleEvent})
- false를 반환해도 기본 동작을 방지할 수 없으므로 e.preventDefault() 를 초기에 명시적으로 호출해줘야함. (e는 합성 이벤트 객체)
- DOM 엘리먼트가 처음 렌더링 될 때 이벤트 리스너가 제공된다. (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>
);
}
}
Javascript의 Class 문법은 기본적으로 this가 undefined 이다.
그렇기에, constructor에서 bind() 처리가 필요하다. (handleClick 옆에 ()가 없는 경우)
혹은, 메서드를 일반함수가 아닌 Arrow Function 으로 선언하거나, 이벤트 리스너 호출 시에 Arrow Function을 취하는 방법도 있다.
// 1) Arrow Function 선언 (실험적인 문법)
handleClick = () => {
console.log('this is:', this);
}
render() {
return (
<button onClick={this.handleClick}>
Click me
</button>
);
};
// 2) Arrow Function 지정
return (
<button onClick={() => this.handleClick()}>
Click me
</button>
);
7. 조건부 렌더링
JSX도 Javascript 이므로 if 조건문을 통해 컴포넌트들을 조건부로 반환할 수 있다. 또한, 이를 변수에 담아 JSX 내에 다시 적용할 수 있다.
class LoginControl extends React.Component {
constructor(props) {
// ...
}
handleLoginClick() {
this.setState({isLoggedIn: true});
}
handleLogoutClick() {
this.setState({isLoggedIn: false});
}
render() {
const isLoggedIn = this.state.isLoggedIn;
/* 1) button 변수에 조건부로 컴포넌트 저장 */
let button;
if (isLoggedIn) {
button = <LogoutButton onClick={this.handleLogoutClick} />;
} else {
button = <LoginButton onClick={this.handleLoginClick} />;
}
return (
<div>
<Greeting isLoggedIn={isLoggedIn} />
// 2) button 변수를 JSX 내에서 적용
{button}
</div>
);
}
}
이를 인라인으로 작성하여 가독성을 높이는 방법이, && 논리 연산자와 삼항 연산자 등의 방법인 것이다.
이외에도, 컴포넌트 자체의 렌더링을 막기 위해 특정값(props 등)에 따라 null을 반환하는 방법도 있다.
function WarningBanner(props) {
if (!props.warn) {
return null;
}
return (
<div className="warning">
Warning!
</div>
);
}
8. 리스트와 Key
"key"는 리스트 엘리먼트가 포함해야 하는 특수한 문자열 어트리뷰트이다. 이는, React가 어떤 항목을 변경, 추가, 삭제할지 식별하는 것을 돕기 위함이다.
key는 엘리먼트에 안정적인 고유성을 보유하기 위함이며, 배열의 문자값 사용을 권장하며, 최후의 보루는 인덱스를 사용하는 것이다.
key를 설정하지 않으면 React는 기본적으로 인덱스를 key로 설정하나 이는 Side Effect를 유발할 우려가 있으므로 권장하지 않는다.
key는 중복되지 않는 고유한 값으로 설정해야 하므로 통상 Id를 활용한다. 또한, 리스트가 아닌 배열을 map()하는 컨텍스트에 써야한다.
9. 폼(Form)
HTML의 <form>은 자체가 내부 상태를 가지므로, React의 다른 DOM 엘리먼트들과는 조금 다르게 동작한다.
- 제어 컴포넌트(Controlled Component)
HTML에서 <input>, <textarea>, <select> 와 같은 폼 엘리먼트는 일반적으로 사용자의 입력을 기반으로 자신의 state를 관리한다.
React는 state가 일반적으로 컴포넌트의 속성으로 유지되며, setState() 함수에 의해 업데이트한다.
우리는, React state를 "신뢰 가능한 단일 출처(single source of truth)" 로 만들기 위해 두 요소를 결합하는 것이다.
이러한 방식으로 React에 의해 갑싱 제어되는 입력 폼 엘리먼트를 "제어 컴포넌트" 라고 한다.
- <input> 태그 : value, onChange 각각에 state, 이벤트 핸들러를 연결하는게 보통이다.
- <input> 다중입력 제어 : 각 <input>에 name을 부여하여, 해당 name과 state key값을 통일시키면 수정이 용이하다.
- <textarea> 태그 : <input>과 마찬가지이다. HTML에서는 pair 태그 내 값을 썼다면, React는 value 어트리뷰트로 설정한다.
- <select> 태그 : 드롭다운 목록들(<option>)을 만든다. HTML은 <option>에 selected 어트리뷰트를 부여했던 반면, React는 <select>의 value 어트리뷰트와 value 값이 일치하는 <option> 이 자동적으로 selected 된다. (multiple이 true면 복수선택)
- <input type="file" /> : 사용자가 파일을 자신의 장치 저장소에서 서버로 업로드하는 용도로, 읽기전용인 비제어 컴포넌트.
자습서에서 비제어 컴포넌트(DOM을 통한 상태관리)의 기법과, <Formik> 이라는 커스텀 라이브러리 등을 추가적으로 소개한다.
10. State 끌어올리기
이 장에서 소개하는 코드나 내용은 많지만 주된 내용은 "공통되는 state와 핸들러 메서드를 부모 컴포넌트가 통합 관리" 한다는 점이다.
state가 필요한 각각의 컴포넌트가 관리하는 게 맞지만, 공통요소를 부모에서 관리하면 효율성과 유지보수가 유리해진다.
이렇게 하는 이유는, React가 부모 -> 자식으로만 데이터를 전달할 수 있는 "단방향 데이터 바인딩" 방식을 채택하기 때문이다.
이는 다른 프레임워크의 양방향에 비해, props에 상당히 의존하는 많은 코드량을 보이지만 버그 위험성은 매우 낮다는 장점이 있다.
(계산값의 소스가 state라면 라이프사이클 꼬임이나 리렌더 등으로 인해 오류가 발생할 수 있는 반면, props는 읽기 전용 값이므로 이러한 버그 위험성이 줄어든다.)
11. 합성 vs 상속
상속은 클래스 인스턴스에 기능을 추가한 확장된 인스턴스를 만드는 방법이다. 하지만, 공식문서는 컴포넌트간의 합성을 권장한다.
- 컴포넌트에 다른 컴포넌트를 담기
function FancyBorder(props) {
return (
<div className={'FancyBorder FancyBorder-' + props.color}>
{props.children}
</div>
);
}
한 컴포넌트가 예측할 수 없는 자식 컴포넌트를 담을 때 props.children을 사용하면 된다. 이 컴포넌트가 랩핑하는 자식요소가 들어온다.
function SplitPane(props) {
return (
<div className="SplitPane">
<div className="SplitPane-left">
{props.left}
</div>
<div className="SplitPane-right">
{props.right}
</div>
</div>
);
}
function App() {
return (
<SplitPane
left={
<Contacts />
}
right={
<Chat />
} />
);
}
복수의 컴포넌트를 담을 때 여러 개의 구멍을 뚫는 방법이다. 심오한 방법이 아닌, props로 컴포넌트를 하달한다고 이해하면 되겠다.
이렇게 props로 다양한 값을 받는다는 React의 강점을 활용해, 원시타입 뿐만 아니라 클래스, 함수, 객체, 컴포넌트 등을 하달하여 재활용성을 높이기를 권장한다.
* 상속은 Facebook의 수천 개의 컴포넌트 사이에도 모범적인 사례를 찾기 어렵다. 그만큼, props와 합성이 명시적이고 안전하다는 것이다.
12. React로 사고하기
React는 Facebook, Instagram으로 입증된 JS기반의 대규모, 고속 웹 어플리케이션을 만드는 가장 좋은 방법이다.
React의 장점 중 하나가 이 어플리케이션 설계 방식이다. 이러한 설계방식(사고)을 순차적으로 정리한 페이지다.
- 0단계 : 목업(mock up)과 API
디자인 시안을 목업이라고도 칭하는 것 같다. 또한, JSON 형태로 된 API 역시 백엔드에서 받을 수 있을 것이다.
[
{category: "Sporting Goods", price: "$49.99", stocked: true, name: "Football"},
{category: "Sporting Goods", price: "$9.99", stocked: true, name: "Baseball"},
{category: "Sporting Goods", price: "$29.99", stocked: false, name: "Basketball"},
{category: "Electronics", price: "$99.99", stocked: true, name: "iPod Touch"},
{category: "Electronics", price: "$399.99", stocked: false, name: "iPhone 5"},
{category: "Electronics", price: "$199.99", stocked: true, name: "Nexus 7"}
];
- 1단계 : UI를 컴포넌트 계층 구조로 나누기
컴포넌트(및 하위 컴포넌트)에 박스를 그리고 각각의 명칭을 정한다. (디자이너의 포토샵 레이어 이름이 컴포넌트 이름이 될 수 있음.)
이를 컴포넌트로 구분하기 위해선 "단일 책임 원칙"을 준수한다. 한 컴포넌트는 한 가지 일을 하는게 이상적이며, 규모가 커지면 분리한다.
- FilterableProductTable(노란색): 예시 전체를 포괄합니다.
- SearchBar(파란색): 모든 유저의 입력(user input) 을 받습니다.
- ProductTable(연두색): 유저의 입력(user input)을 기반으로 데이터 콜렉션(data collection)을 필터링 해서 보여줍니다.
- ProductCategoryRow(하늘색): 각 카테고리(category)의 헤더를 보여줍니다.
- ProductRow(빨강색): 각각의 제품(product)에 해당하는 행을 보여줍니다.
- 2단계 : React로 정적인 버전 만들기
앱을 실제로 구현하는데 있어, 가장 먼저 데이터 모델을 가지고 UI만 렌더링되는 버전을 만드는 것이 좋다. (아무런 동작도 하지 않는)
- 정적인 UI는 생각은 적지만 타이핑은 많고, 상호작용 기능은 생각은 많지만 타이핑이 적기 때문이다.
- 데이터 모델을 앱의 정적버전을 만들기 위해, 컴포넌트를 통해 props로 데이터를 전달한다. state는 상호작용을 위해서만 사용한다.
- 앱 제작은 하향식(Top-Down)과 상향식(Bottom-Up)이 있다. 통상, 하향식을 권장하나 대규모 앱은 상향식과 테스트 반복.
- 완료되면, 데이터의 대략적인 전달방법과 재사용 가능한 컴포넌트들이 구성된다. 데이터는 props, 메서드는 render()만 있는 상태다.
- 3단계 : UI state에 대한 최소한의 (완전한) 표현 찾아내기
UI를 상호작용하여 데이터 모델을 변경하는 기능이 필요하다. React에서는 이를 state와 setState() 함수를 통해 변경한다.
이 state는 중복되지 않도록 최소한으로 활용한다. (가령, To-do List에선 배열만 저장하고, 아이템 갯수나 특정값은 로직으로 가져온다.)
- 부모로부터 props를 통해 전달됩니까? 그렇다면, 확실히 state가 아닙니다.
- 시간이 지나도 변하지 않나요? 그렇다면, 확실히 state가 아닙니다.
- 컴포넌트 안의 다른 state나 props를 통해 계산할 수 있나요? 그렇다면, state가 아닙니다.
- 4단계 : state의 위치 찾기
다음으로, 어떤 컴포넌트가 state를 변경하거나 소유할 지 찾으면 된다.
- state 기반으로 렌더링되는 모든 컴포넌트를 찾으세요.
- 공통 컴포넌트(common owner component)를 찾으세요. (계층구조에서 특정 state가 있어야 하는 컴포넌트들의 최상위 컴포넌트)
- 공통 혹은 더 상위에 있는 컴포넌트가 state를 가져야 합니다.
- 소유할 적절한 컴포넌트를 찾지 못하였다면, state를 소유하는 컴포넌트를 하나 만들어서 공통 컴포넌트 상위 계층에 추가한다.
- 5단계 : 역방향 데이터 흐름 추가하기
지금까지는, React의 단방향 데이터 바인딩 기반을 props와 state로 구현하였다.
마지막으로, 하위 컴포넌트가 상위 state를 업데이트하는 메서드들을 추가해주면 된다. 이 핸들러 함수를 부모 컴포넌트에 만들고, props를 통해 자식 컴포넌트로 전달해주면 된다.
설치 섹션은 전반적인 세팅 방법들에 대해 알려주고 있다. 물론, 기본적으로 쓰는 CRA와 Latest 등은 고정적이긴 하다.
하지만 이러한 설정들의 동일선상에 다른 종류들이 있다는 점을 짚고 넘어갈 수 있었다.
앞으로는 좀 더 개발에 연관된 기술적인 내용들이 기술될 것으로 보인다!
[출처] React 공식문서(번역본) : ko.reactjs.org/docs/release-channels.html