[React 자습서]
개요
리액트란
사용자 UI를 구축하기위한 효율적이고 유연한 Javascript 라이브러리이다.
특히, component라는 코드의 파편을 이용해 복잡한 UI구성을 돕는다.
리액트 컴포넌트
class ShoppingList extends React.Component {
render() {
return (
<div className="shopping-list">
<h1>Shopping List for {this.props.name}</h1>
<ul>
<li>Instagram</li>
<li>WhatsApp</li>
<li>Oculus</li>
</ul>
</div>
);
}
}
위 코드는 JSX방식
으로 작성되었는데, JSX 내부의 중괄호 안에 javascript 표현식을 작성함으로써
React Element
로 자바스크립트 객체를 만든다.
여기서 ShoppingList는 React Component class
또는 React Component Type
이라고 불린다.
props
라는 매개변수를 불러와 render()
함수를 통해 표시할 html 태그들을 반환해준다.
ShoppingList
라는 react Component
는 div, li 태그를 가진 DOM component를 렌더링하는데,
<ShoppingList />
로 해당 react Component
의 모든 요소를 참조할 수 있다.
이러한 react Component
들은 독립적으로, 다른 파일에서도 쓰일 수 있기에
문제를 작게 쪼개 단순한 component들을 만들고, 조합해 복잡한 UI를 만들 수 있다!!
Render()함수 작동원리
render함수는 return 뒤의 내용을 반환한다.
이 때, 반환하는 방식은 렌더링할 내용을 경량화한 React Element 형식으로 반환하는데,
많은 개발자들이 JSX라는 특수한 문법을 사용해 React의 구조를 쉽게 작성한다고 한다.
/*#__PURE__*/
React.createElement(
"div",
{ className: "shopping-list" },
React.createElement("h1", null, "Shopping List for ", props.name),
React.createElement(
"ul",
null,
React.createElement("li", null, "Instagram"),
React.createElement("li", null, "WhatsApp"),
React.createElement("li", null, "Oculus")
)
);
React.createElement('태그', {클래스 또는 아이디}, 컨텐츠)
2번째 매개변수에 null이 선언된다면 클래스 또는 아이디가 없다는 것을 의미한다.
3번째, 컨텐츠 매개변수에 다른 React Element를 배치함으로써 children 요소를 받을 수 있다.
Props로 데이터 전달하기
Square
class Square extends React.Component {
render() {
return <button className="square">{this.props.value}</button>;
}
}
버튼을 리턴한다.
여기서 {this.props.value}
의 props는 임의의 입력이다.
위를 예시로 들면, Square라는 리액트 컴포넌트를 생성할때,
매개변수가 들어올 자리라고 생각하면 된다. 자세한 내용은 Board 컴포넌트까지 이해해야한다.
Board
class Board extends React.Component {
renderSquare(i) {
return <Square value={i} />;
}
render() {
const status = "Next player: X";
return (
<div>
<div className="status">{status}</div>
<div className="board-row">
{this.renderSquare(0)}
{this.renderSquare(1)}
{this.renderSquare(2)}
</div>
<div className="board-row">
{this.renderSquare(3)}
{this.renderSquare(4)}
{this.renderSquare(5)}
</div>
<div className="board-row">
{this.renderSquare(6)}
{this.renderSquare(7)}
{this.renderSquare(8)}
</div>
</div>
);
}
}
Board 컴포넌트는 renderSquare
와 render
함수로 이루어져있다.
쉽게 이해하기 위해서는 밑의 render부터 봐야하는데,
render()함수에서 {this.renderSquare(0)}
로 Board
컴포넌트에서 renderSquare
라는 함수를 호출해 board를 그리고 있다.
그리고 호출된 renderSquare(i)는 매개변수로 값을 입력받아 <Square value={i} />
형식으로
Square
컴포넌트를 매개변수 i를 넣어 리턴해준다.
{this.props.value}
다시 Square에서 this.props.value를 보자. props.value는 이 컴포넌트가 호출될때 입력된 매개변수 중
value의 매개변수 값을 가져온다는 의미이다.
이렇게 Board와 Square컴포넌트의 흐름을 봤는데, Board 컴포넌트를 부모 컴포넌트,
Square컴포넌트를 자식 컴포넌트라고 부른다.
부모컴포넌트부터 자식컴포넌트까지 props를 전달하면서 react 앱이 어떻게 작동하는지 조금은 감이 온다.
사용자와 상호작용
리액트 컴포넌트는 생성자에 this.state를 설정함으로써 state라는 변수를 가질 수 있다.
9개의 사각형이 각각 인스턴스화되어 다른 객체를 이루어야하고,
인스턴스 프포퍼티로 사용되는게 props와 state이다.
class Square extends React.Component {
constructor(props) {
super(props);
this.state = {
value: null,
};
}
사용자와 상호작용하기 위해서는 버튼에 이벤트리스너를 달아주어야한다.
{() => 함수}
에서 =>를 자주 까먹는다. 잊지말자.
render() {
return (
<button
className="square"
onClick={() => this.setState({value: 'X'})}>
{this.state.value}
</button>
);
}
정리하면, Square 컴포넌트가 호출되면 생성자가 초기화되면서 빈 사각형의 버튼이 리턴된다.
게임 완성하기
State를 부모 컴포넌트로 끌어올리기
이제 각 Square 컴포넌트가 각각 state를 유지하고있다.
이제, 승자를 확인하기 위해서 각 Square 컴포넌트의 value를 한 곳에 유지해야한다.
Board 컴포넌트가 각 square의 state를 요청해도 되지만, 코드를 이해하기가 어렵고 버그에 취약하며 리팩토링이 어렵기때문에 선호되는 방식은 아니다.
그래서 각 Square가 아닌 부모 컴포넌트인 Board 컴포넌트에 게임의 상태를 저장하는 것이 가장 좋은 방법이다.
Board 컴포넌트는 각 Square에게 prop을 전달해 무엇을 표시할 지 알려준다.
자식 컴포넌트(Square)로부터 데이터를 모으거나, 두개의 자식 컴포넌트가 서로 연결되려면 부모 컴포넌트(Board)가 공유 state를 정의해야한다.
그러기 위해 부모 컴포넌트 Board에 prop을 사용해 자식 컴포넌트 square에게 state를 전달한다.
이를 통해 자식컴포넌트간 혹은, 자식 컴포넌트와 부모 컴포넌트가 동기화 할 수 있다.
Board 수정
부모컴포넌트인 Board가 자식 컴포넌트에게 state를 전달하기 위해서는 Board가 생성자를 가져야한다.
class Board extends React.Component {
constructor(props) {
super(props);
this.state = {
squares: Array(9).fill(null),
};
}
}
renderSquare(i) {
return <Square value={i} />
}
이제 Board의 state에서 squares가 배열의 형태로 9개 칸의 정보를 담는다.
이제 Board 컴포넌트에서 Square 자식 컴포넌트에게 정보를 전달해야한다.
현재 renderSquare()함수는 그 자체가 매개변수로 i를 입력받아 Square 컴포넌트를 i를 그린 값으로 그리고 클릭되면 setState로 Square의 state를 ‘X’로 변경하면서
X사각형으로 그려진다.
이제는 부모컴포넌트가 array의 형태로 Square 컴포넌트 버튼 안의 정보를 가지고 있기 때문에,
Square 자식 컴포넌트에게 Board state의 정보를 전달하기 위해 i를 배열의 인덱스로 삼아
renderSquare(i) {
return <Square value = {this.state.squares[i]} />;
}
바꿔준다.
중간정리
- Board가 생성되며 null 값을 가진 array의 형태로 this.state.squares 생성.
- Board는 return으로 리액트 컴포넌트를 리턴하는데, 각 박스가 renderSquare(i)호출
- 각 renderSquare는 Square 컴포넌트를 value값이 this.state.squares[i]인 상태로 리턴한다.
- renderSquare로 리턴된 Square컴포넌트는 클릭되면 square.state에 따라 onclick으로 ‘x’로 변경한다.
이제 각 사각형이 클릭되면 발생하는 변화를 Board에서 정의한다.
각 renderSquare가 클릭되면, 변화를 만드는 함수를 호출되게하도록 변경한다.
renderSquare(i) {
return (
<Square
value={this.state.squares[i]}
onClick={() => this.handleClick(i)}
/>
)
}
Square가 Board로부터 props 전달받는 법
이제 각 Square에서 생기는 변화는 Board의 renderSquare함수에서 props의 형태로 전달되고, 관리된다.
따라서
- Square 컴포넌트는 state를 유지할 필요가 없으므로 생성자 제거
- Sqaure 컴포넌트의 state가 제거되고, value는 props로 받으므로 this.state.value를 this.props.value로 변경
- this.setState()가 변경을 관리했으나, square 컴포넌트의 state가 삭제되었으므로 this.props.onClick()으로 변경
이제 부모컴포넌트로부터 자식컴포넌트에게 state가 전달된다.
이 과정을 짧게 요약하면
부모컴포넌트에서 생성자로 state 생성 =>
부모컴포넌트가 props의 형태로 자식 컴포넌트에게 state를 전달
이런 방식으로 전달되는 것을 알 수 있다.
handleClick
지금 handleClick(i)함수가 정의되지않아 박스를 클릭하면 파일이 깨지는데,
변화를 만드는 handleClick()함수를 Board 컴포넌트에 추가한다.
handleClick(i) {
const squares = this.state.squares.slice();
squares[i] = 'X';
this.setState({squares: squares});
}
- Square의 button 에서 Onclick()을 감지하면 Onclick 이벤트 핸들러가 this.props.onClick() 호출
- Square의 props는 renderSquare함수에서 정의되었다.
- Board에서 Square로 onClick={() => this.handleClick()}을 전달했으므로 Board의 handleClick() 호출
- 이때, 현재 squares를 this.state.squares.slice()로 복사해
배열의 인덱스 i를 ‘X’로 변경하고 - setState로 this.state.squares에 변경된 squares를 저장한다.
불변성이 중요한 이유
handleClick(i)
코드를 보면
const squares = this.state.squares.slice();
로 새로운 const변수(이름만 같은 새로운 변수)에 board의 state squares를 복사해 할당했다.
그리고 이를
this.setState({ squares: squares });
board 컴포넌트의 state를 업데이트하는데, 앞의 squares는 state를,
뒤의 squares는 위에 새로운 변수를 의미하므로 헷갈리지 말자.
직접적으로 squares를 변경하는 것이 아닌,
새로운 변수를 만들어 기존 변수를 변경하지않는 방법을 사용하는데,
그 장점은
1.복잡한 특징을 단순하게
현재 튜토리얼 웹앱의 핵심 기능은 시간여행 기능을 구현하는 것이다.
이 기능은 기 웹앱에서만 필요한 것이 아닌, 모든 어플리케이션이 기본적으로 갖고 있는 기능인데,
데이터를 직접적으로 변환하지 않는 것은 이전의 데이터를 재사용할 수 있게 만든다.
2. 변화감지
직접적으로 수정된 객체에서 변화를 감지하는 것은 어렵다.
직접적으로 수정된 객체에서 변화를 감지하기 위해서는 이전 객체의 복사본과 현재 수정된 객체를 비교하고
전체 객체 트리를 돌아야 한다.
하지만 const로 선언된 복사객체에서 변화를 감지하는 것은 쉽다.
현재 const로 선언된 squares객체와 현재 this.state.squares를 비교해 다르면
객체는 변한 것이다.
3. React에서 다시 렌더링하는 시기를 결정
리액트에서 순수 컴포넌트를 만드는 데 도움을 준다.
변하지 않는 데이터는 변경이 이루어졌는지 쉽게 판단할 수 있고, 이를 바탕으로
컴포넌트가 다시 렌더링할지를 결정할 수 있다.
함수 컴포넌트
Square를 함수 컴포넌트로 바꿔보자
function Square(props) {
return (
<button className="square" onClick={props.onClick}>
{props.value}
</button>
);
}
함수 컴포넌트는 더 간단하게 컴포넌트를 작성하는 방법이다.
state 없이 render 함수만을 가진다.
render함수만을 가진다는 의미는, React.component를 확장하는 클래스를 정의하는 대신,
매개변수 props를 입력받아 렌더링할 대상(태그)를 반환을 함수를 작성한다.
클래스로 작성하는 것보다 빠르게 작성할 수 있고, 많은 컴포넌트를 함수 컴포넌트로 표현할 수 있다.
순서 만들기
게임판에서 O를 표시하기 위해서 첫 번째 차례를 ‘X’로 시작한다.
Board 생성자의 초기 state를 수정하는 것으로 기본값을 설정할 수 있다.
class Board extends React.Components {
constructor(props) {
super(props);
this.state = {
squares: Array(9).fill(null),
xIsNext: true,
};
}
플레이어가 수를 둘 때마다 xIsNext가 false, true로 바뀌면서 다음 플레이어를 정의한다.
그리고 게임의 state가 저장될 것이다.
플레이어가 수를 두는 것을 담당하는 함수는 handleClick(i) 이므로 이를 수정한다.
handleClick(i) {
const squares = this.state.squares.slice();
squares[i] = this.state.xIsNext ? 'X' : 'O';
this.setState({
squares: squares,
xIsNext: !this.state.xIsNext,
});
}
Board 컴포넌트의 status로 다음 플레이어를 입력받아야한다. 현재 다음 플레이어에 대한 정보는
state의 xIsNext에 저장되어있으므로
render() {
const status = 'Next player: ' +
(this.state.xIsNext ? 'X' : 'O')
}
승자 결정하기
이제 승자를 확인해야한다.
function calculateWinner(squares) {}
const lines = [
[0, 1, 2],
[3, 4, 5],
[6, 7, 8],
[0, 3, 6],
[1, 4, 7],
[2, 5, 8],
[0, 4, 8],
[2, 4, 6],
];
for (let i = 0; i < lines.length; i++) {
const [a, b, c] = lines[i];
if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) {
return squares[a];
}
}
return null;
승자가 발생하는 모든 경우의 수를 만들어 놓고, 각각의 경우에 대해 현재 squares에 입력되어 있는 값들을 비교해 모두 같다면 true가 되며, 승자를 리턴한다.
하지만, 없다면 for문을 빠져나오며 null을 리턴할 것이다.
그리고
render() {
const winner =
calculateWinner(this.state.squares);
let status;
if (winner) {
status = 'Winner: ' + winner;
} else {
status = 'Next player: ' +
(this.state.xIsNext ? 'X' : 'O')
}
winner변수에 calculateWinner를 저장하는데, 이는 ‘X’,’O’,’null’ 중 하나이다.
if문에서 winner가 ‘X’ 나 ‘O’ 중 하나라면 true로 인식되며 코드가 실행되고,
status에 winner를 출력한다.
아직 승자가 나오지 않았거나, 실행이 끝났는데도 승자가 없다면 winner가 null로 false가 될 것 이므로, 다음 플레이어가 출력된다.
누군가 승리하거나, Square가 다 채워지면 게임이 지속되면 안되므로
handleClick 함수가 무시되도록 변경한다.
handleClick(i) {
const squares = this.state.squares.slice();
if (calculateWinner(squares) || squares[i]) {
return;
}
squares[i] = this.state.xIsNext ? 'X' : 'O';
this.setState({
squares: squares,
xIsNext: !this.state.xIsNext,
});
}
동작 되돌리기
동작에 대한 기록 저장
slice()로 새로운 객체를 생성해 state를 변경했으므로, 과거의 기존 squares에 접근이 가능하다!!
사용자가 버튼을 클릭하기 전에 현재 squares 배열들을 history라는 다른 배열에 저장한다.
Game 컴포넌트에 의해 Board 컴포넌트가 호출되고, 게임이 진행된다.
최상위 컴포넌트인 Game 컴포넌트에 history state를 두자.
class Game extends React.Component {
constructor(props) {
super(props);
this.state = {
history: [
{
squares: Array(9).fill(null),
},
],
xIsNext: true,
};
}
render() {
return (
<div className="game">
<div className="game-board">
<Board />
</div>
<div className="game-info">
<div>{/* status */}</div>
<ol>{/* TODO */}</ol>
</div>
</div>
);
}
}
state 끌어올리기
최상위 컴포넌트 Game에게로 Board 컴포넌트의 state를 끌어올린다.
Game컴포넌트가 Board 컴포넌트에게 squares와 onClick에 대한 정보를 props로 전달해야하므로
- Board의 생성자를 삭제
- Board의 renderSquare 함수 안에서 value={this.props.squares[i]}
- Board의 renderSquare 함수 안에서 onClick={() => this.props.handleClick(i)}
Game 컴포넌트가 게임의 모든 상태를 관리한다.
따라서 게임의 상태를 관리했던 Board의 render() 함수에서 return() 전까지 코드를 Game컴포넌트의 render()함수 아래로 이동한다.
--Game--
render() {
const history = this.state.history;
const current = history[history.length - 1];
const winner = calculateWinner(current.squares);
let status;
if (winner) {
status = 'Winner: ' + winner;
} else {
status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');
}
handleClick()함수를 Board 컴포넌트에서 Game 컴포넌트로 이동한다.
handleClick(i) { //눌리면 실행되는 함수
const history = this.state.history;
const current = history[history.length - 1];
const squares = current.squares.slice();
if (calculateWinner(squares) || squares[i]) {
return;
}
squares[i] = this.state.xIsNext ? 'X' : 'O';
this.setState({
history: history.concat([{
squares: squares,
}]),
xIsNext: !this.state.xIsNext,
});
}
사용자가 버튼을 클릭해 handleClick()함수가 실행되면 이전 데이터는 history state에 저장한다.
이때, 현재 상태는 current는 history에서 마지막 정보이므로, 이를 기준으로 승자를 계산한다.
Board 컴포넌트의 역할은 game 컴포넌트로부터 정보를 받아 square 컴포넌트를 그리는 역할을 한다.
따라서 renderSquare함수와 render함수만을 가진다.
과거 이동 표시
Javascript의 map() 함수는 배열을 다른 데이터와 함께 매핑이 가능하다.
const numbers = [1, 2, 3];
const doubled = numbers.map((x) => x * 2);
//doubled = [2,4,6]
이를 통해 history의 배열들을 react 버튼 엘리먼트로 매핑해 과거로 돌아가는 버튼 목록을 표시하자.
const moves = history.map((step,move) => {
const desc = move ?
'Go to move #' + move;
'Go to game start';
return (
<li>
<button onClick={() => this.jumpTo(move)}>{desc}</button>
</li>
);
});
이 코드의 문제점은 li 태그에 key prop이 없다
라는 것이다.
리액트는 리스트를 렌더링할때 리스트 아이템에 대한 정보를 저장한다.
그리고 리스트를 렌더링할 때, 어떤 정보가 추가되고, 제거되고, 재배열, 업데이트 되었는지 결정한다.
리액트는 이걸 결정할 때, key
값을 기준으로 렌더링한다.
이전에 없던 key
값이 생겼다? -> 리엑트가 해당 키를 가진 컴포넌트를 생성한다.
있던 key
값이 사라졌다? -> 리엑트가 해당 키를 가진 컴포넌트를 삭제한다.
key
값을 li 태그에 만드는 것은 리엑트가 효율적으로 컴포넌트를 삭제하고, 추가하는 것에 큰 도움을 준다.
const moves = history.map((step, move) => {
const desc = move ? "Go to move #" + move : "Go to game start";
return (
<li key={move}>
<button onClick={() => this.jumpTo(move)}>{desc}</button>
</li>
);
});
각 li 태그에 key 값으로 move를 주자.
시간여행 구현하기
현재 몇 번까지 진행했는지 상태를 유지하기 위해 Game 컴포넌트에 stepNumber state를 추가한다.
class Game extends React.Component {
constructor(props) {
super(props);
this.state = {
history: [
{
squares: Array(9).fill(null),
},
],
stepNumber: 0,
xIsNext: true,
};
}
}
Game의 stepNumber state를 업데이트하기위한 jumpTo 함수를 정의한다.
이 함수에서 stepNumber가 짝수일 때 마다 xIsNext를 true로 설정한다.
jumpTo(step) {
this.setState({
stepNumber: step,
xIsNext: (step % 2) === 0,
});
}
여기서 setState함수에 history를 다시 설정하지 않는 이유는, 이 함수는 history state가 변하는 것과는 별도이기 때문이다.
setState() 메서드는 언급된 state만 변경할 뿐, 다른 state들은 그대로 둔다.
stepNumber state는 현재 사용자가 몇 번때 step을 밝고 있는지 알려준다.
이제 이 stepNumber 를 없데이트하기 위해서 사용자가 박스를 클릭했을 때 실행되는 함수 handleClick() 함수가
stepNumber state를 수정하도록 해야한다.
또한 history 변수가 stepNumber가 바뀌면 없어져야하는 미래의 기록을 가지지 않도록 해야한다.
handleClick(i) {
const history = this.state.history.slice(0, this.state.stepNumber + 1);
const current = history[history.length - 1];
const squares = current.squares.slice();
if (calculateWinner(squares) || squares[i]) {
return;
}
squares[i] = this.state.xIsNext ? 'X' : 'O';
this.setState({
history: history.concat([{
squares: squares
}]),
stepNumber: history.length,
xIsNext: !this.state.xIsNext,
});
}
그리고 현재 렌더링할 대상을 정하는 render()함수의 current 변수를 마지막값이 아닌, 현재 stepNumber로 재정의한다.
render() {
const history = this.state.history
const current = history[this.state.stepNumber]
const winner = calculateWinner(current.squares);
}
댓글남기기