9) ENS Project - 프론트엔드 후딱 학습하기(React Deep Dive!!)
2024.10.15 - [Front/React.js] - 8) ENS Project - 프론트엔드 후딱 학습하기(리액트 컴포넌트, JSX, 속성, 상태)
저번시간에 이어서 오늘은 더욱 더 깊은 심화과정 입니다! 저장을 잘하면서 포스팅 해보도록 하겠습니다:)
Point!!
- JSX를 꼭 사용하지 않아도 되는 이유
- 컴포넌트 트리를 설계하는 방법 + 프로젝트를 더 효율적으로 구성하는 방법
- State 사용의 상급 개념과 특정 상급 개념을 살펴봅니다.
ㄴ 저번시간엔 useState()를 사용했습니다.
- 자주 사용되는 중요 패턴과 Best Practices를 살펴봅니다.
JSX를 꼭 사용하지 않아도 되는 이유?
React Code는 Build Process를 통해서 변경 됩니다.
결과적으로 배포가능한 파일로 말이죠!
그래서 비표준적인 코드가 JSX에서 사용이 되곤하는데 문제가 없습니다.
( ex. class=가 JSX에서는 className=으로 사용)
React개발자로서 알아야할 점은?
- JSX의 도움 없이도 리액트 앱을 빌드 할 수 있다는 점 입니다.
- 그러므로 위에서 언급한 Build Process이 없어도 됩니다.
- 물론 대다수의 리액트 프로젝트는 JSX를 사용 합니다.
- 그러나 적어도 이론상으로 리액트 프로젝트 빌드에 JSX는 굳이 필요하지 않습니다.
컴포넌트를 바로 작성해서 빌드없이 JSX를 사용하지 않는 접근방식을 사용할 수 있습니다.
단, 그리 직관적이지는 않습니다.
React.crateElement(
'div'
{ id: 'content' },
React.createElement(
'p',
null,
'Hello World'
)
);
JSX를 사용하는 접근방식이 보통 사용하기 쉽고 가독성이 훨씬 뛰어납니다!
<div id="content">
<p> Hello World!</p>
</div>
요런게 있다~라고만 알고 넘어가면 될 듯 합니다!
다음으로 JSX와 JSX코드 작성법에 대해서 알아봅시다!
App.jsx에서
return (
<div>
<Header />
<main>...
</main>
</div>
);
<div> tag를 지운다면 오류가 납니다.
그 이유는 JSX표현식은 하나의 Parent element를 꼭 반드시 가지고 있어야 합니다.
또한 div를 지운다면 하나의 리턴이 아닙니다. (<Header /> <main>..</main>)
자바스크립트는 하나의 리턴값을 가지고 있습니다.
<div>는 값들을 감싸는 객체 혹은 배열로 생각할 수 있습니다.
이런 한계점으로 인해 DOM구조에는 div가 두개 생깁니다.
오류는 안나지만 불필요한 내용 입니다.
그래서 리액트는 대안으로 fragment라는 컴포넌트를 사용 합니다.
사용은 react 내장 컴포넌트인 Fragment를 import해줍니다.
import { useState, Fragment } from 'react';
return (
<Fragment>
<Header />
<main>...
</main>
</Fragment>
);
최신 버전의 프로젝트에서는 아래와 같이 매우 심플하게 표현 합니다.
return (
<>
<Header />
<main>...
</main>
</>
);
다음으로 컴포넌트에 대해서 알아보겠습니다.
하나의 컴포넌트에서 다양한 요소를 관리하고 있다면 나눠주는게 좋습니다.
더 작은 단위의 하위 컴포넌트로 여러개 나누는게 좋습니다.
모든 프로그래밍에서 강조하는 부분이기도 합니다.
책임을 한곳에서 모두 지지말고 분할하라!
OOP에서는 SOLID개념중에 하나인 SRP가 되겠죠! ㅎㅎ
큰 단위의 앱을 잘게 나누기!
기능별로 나누는게 매우 좋은 전략입니다.
CoreConcepts.jsx
ㄴ CORE_CONCEPS Data : import { CORE_CONCEPTS } from '../data.js';
ㄴ CoreConcept Component : imprt CoreConcept from 'CoreConcept.jsx';
App.jsx
ㄴ CoreConcepts.jsx : import CoreConcepts from './components/CoreConcepts.jsx';
return (
...
<main>
<CoreConcepts />
...
)
동일하게 TabButton과 tabContent를 처리!
Examples.jsx
ㄴ export default function Examples() { return () }
ㄴ import { useState } from 'react';
ㄴ EXAMPLS 상수 데이터 : import { EXAMPLES } from '../data.js';
ㄴ import TabButton from './TabButton.jsx';
App.jsx
ㄴ import Exampls from './components/Examples.jsx';
return (
...
<main>
<CoreConcepts />
<Examples />
</main>
)
이렇게 나눠지면 App Component가 재실행 되지 않습니다.
Examples Component만 상태관리에 의해 재실행 됩니다.
공통적으로 <section> 부분을 Section component를 만들어서 관리할 수 있습니다.
단, id props의 속성값이 전달하지 않으면 css가 깨집니다.
전달방법은 여러가지가 있으나 아래와 같은 전달 패턴이 있습니다.
forwarded props(전달 속성) 또는 proxy props(대리 속성)
속성을 전달하는 패턴!
export default function Section({ title, children }) {
return (
<section>
<h2>{title}</h2>
{children}
</section>
);
}
JavaScript 내장문법을 사용합니다.
export default function Section({ title, children, ...props }) {
return (
<section>
<h2>{title}</h2>
{children}
</section>
);
}
...props : This JavaScript feature is called "Rest property"
이 컴포넌트 범주에서 사용할 수 있는 모든 다른 props를 모아서 props object(속성객체)로 병합 합니다.
export default function Section({ title, children, ...props }) {
return (
<section {...props}>
<h2>{title}</h2>
{children}
</section>
);
}
wrapper component 작성 시 유용한 패턴 입니다.
Examples.jsx에서는 동일하게 ...props를 사용하고
기존 onSelect prop을 onClick prop로 대체 합니다.
<TabButton
isSelected={selectedTopic === 'components'}
onclick={()=> handleSelect('components')}
>
fragments와 slot 활용 패턴은 리액트 개발 시 매우 중요 합니다!(Lesson-66)
위에서 말한 wrapper component, children props도 알아야 이해가 됩니다.
export default function Tabs({ children, buttons}) {
return (
<>
<menu>{buttons}</menu>
{children}
</>
);
}
다음으로는 다양한 HTML 요소를 동적으로 렌더링 할 수 있게 컴포넌트 식별자를 속성의 값으로 보냅니다.
ex) Examples.jsx에서
<Tabs
buttonsContanier="menu"
buttons={ ..... };
로 선언하고
Tabs.jsx에서는 위에서 선언한 "menu"속성값을 사용합니다. 어떻게?
아래처럼 <menu>{buttons}</menu> 대신 menu를 props로 전달하여 상수로 만들어서 속성값을 전달할 수 있습니다.
매우 중요한 내용이니 기억!!
export default function Tabs({ children, buttons, buttonsContainer}) {
const ButtonsContainer = buttonsContainer;
return (
<>
<ButtonsContainer>{buttons}</ButtonsContainer>
{children}
</>
);
}
속성을 한 상수로 새롭게 설정하지 않고 처음부터 대문자 상수로 ButtonsContanier="menu"로 정하고
Tabs에서 ButtonsContanier로 전달 합니다.
다음으로 틱택톡 게임을 만들어봅니다.
헤더
1) 플레이어의 이름이 보이는 칸, 수정도 가능하게 할 예정
2) 게임판도 있어야 함
3) 게임판 하단에는 로그(기록)을 만들고 마지막엔 결과창을 띄움.
크게 3가지 블록을 만들어야 합니다.
-플레이어 부분
- 게임판
- 로그
그 중 플레이어 부분을 작업해보겠습니다.
function App() {
return <main>
<div id="game-container">
PLAYERS
<ol id="players">
<li>
<span className="player-name">Player1</span>
<span className="player-symbol">X</span>
</li>
<li>
<span className="player-name">Player2</span>
<span className="player-symbol">O</span>
</li>
</ol>
GAME BOARD
</div>
LOG
</main>;
}
export default App
다음으로 플레이어 이름를 동적으로 만들고 수정 버튼도 만듭니다.
위의 소스에서처럼 중복적으로 코드가 발생한다면 컴포넌트를 새로 하나 만드는것이 좋습니다.
여기서 배울 것은? 컴포넌트 분리 & 재사용 가능한 컴포넌트 구축
Player.jsx
export default function Player({name, symbol}) {
return <li>
<span className="player">
<span className="player-name">{name}</span>
<span className="player-symbol">{symbol}</span>
</span>
<button>Edit</button>
</li>
}
App.jsx
import Player from "./components/Player.jsx";
function App() {
return <main>
<div id="game-container">
PLAYERS
<ol id="players">
<Player name="Player1" symbol="X"/>
<Player name="Player2" symbol="O"/>
</ol>
GAME BOARD
</div>
LOG
</main>;
}
export default App
이제 버튼을 작동해보도록 하겠습니다!
여기서 배울것은? State(상태) 활용법
useState()를 사용!
수정 중인지 아닌지 클릭 시 버튼에 onClick으로 함수를 호출한다.
setIsEdting을 사용해야해서 내부 funtion인 hadleEditClick()를 만들어준다.
수정 중이면 input box로 보이게 합니다.
import { useState } from 'react';
export default function Player({name, symbol}) {
const [ isEditing, setIsEditing ] = useState(false);
function handleEditClick(){
setIsEditing(true);
}
let playerName = <span className="player-name">{name}</span>
if(isEditing) {
playerName = <input type="text" required/>;
}
return <li>
<span className="player">
{playerName}
<span className="player-symbol">{symbol}</span>
</span>
<button onClick={handleEditClick}>Edit</button>
</li>
}
React는 상태 업데이트를 스케줄링 합니다.
이때 setIsEditing과 같은 상태 변경함수를 통해 실행
2단계로 수행(set, update)하기 때문에 이 상태 변경은 즉각적으로 수행되지 않고 미래에 수행하고자
상태 변경 스케줄을 조율하는 것 입니다.
리액트 개발자로서 이해해야 하는 중요한 점은 아래의 예상처럼 true, false로 되야하지만 그렇지 않습니다.
const [ isEditing, setIsEditing ] = useState(false );
function handleEditClick(){
setIsEditing(!isEditing); // true
setIsEditing(!isEditing); // false
}
false가 아닌 그대로 true로 동작합니다.
그래서 리액트의 권고사항처럼 함수형태로 사용해야 합니다.
함수형태로 사용하면 최신상태변경을 반영 합니다.
function handleEditClick(){
setIsEditing((editing) => !editing);
}
플레이어이름을 가지고 놀려면 Edit를 눌렀을 때 변경이 가능하고 SAVE를 눌렀을 때 저장되야 합니다.
중요한 포인트는 상태변경을 하나 더 추가 하는 작업과 onChagne에 대해서 알아야 합니다.
const [ playerName, setPlayerName ] = useState(initalName);
onChange에서는 이벤트가 발생을 하기 때문에 function에서는 evnet 파라미터로
keyborad를 눌렀을 때 이벤트를 발생해서 나오는 값들을 전달받을 수 있습니다.
function handleChange(event) {
setPlayerName(event.target.value);
}
let editablePlayerName = <span className="player-name">{playerName}</span>
let btnCaption = 'Edit';
if(isEditing) {
editablePlayerName = <input type="text" required value={playerName} onChange={handleChange}/>;
btnCaption = 'Save';
}
GameBoard.jsx 컴포넌트 추가
const initialGameBoard = [
[null, null, null],
[null, null, null],
[null, null, null],
];
export default function GameBoard() {
return ( <ol id="game-board">
{initialGameBoard.map((row, rowIndex)=> (
<li key={rowIndex}>
<ol>
{row.map((playerSymbol, colIndex)=> (
<li key={colIndex}>
<button>{playerSymbol}</button>
</li>
))}
</ol>
</li>
))}
</ol>
);
}
게임판 상태관리!
이를 위해 useState()를 추가 합니다.
const [gameBoard, setGameBoard] = useState(initialGameBoard);
게임판의 직전 상태에 기반하여 업데이트를 해야합니다.
이전 상태가 지속되도록 하는게 포인트 입니다!
즉, 이전에 선택되었던 필드에 대한 정보를 잃어버리지 않아야 합니다.
배열의 필드 하나만 변경해야 합니다.
const [gameBoard, setGameBoard] = useState(initialGameBoard);
function handleSelectSquare(rowIndex, colIndex) {
setGameBoard((prevGameBoard)=> {
prevGameBoard[rowIndex][colIndex] = 'X';
return prevGameBoard;
});
}
만약 상태가 객체나 배열이라면 해당 상태를 업데이트 할 때 변경 불가능(not mutate)하게 하는것이 좋습니다.
이전 상태를 하나 복제해서 새 객체 또는 배열로 저장해 놓고 복제된 버전을 수정하는 방식 입니다.
이 방식을 추천하는 이유는 만약 상태가 객체 혹은 배열이라면 이는 자바스크립트 내의 참조 값 입니다.
이런 방식으로 업데이트한다면 메모리 속의 기존 값을 바로 변경하게 되는데
이 시점은 리액트가 실행하는 예정된 상태 업데이트보다 이전에 일어나게 됩니다. (참고)
prevGameBoard는 기존 배열 요소를 메모리에 담고있습니다.
새로운 변수 updatedBoard에다가 기존 배열에 있던 요소(prevGameBoard)를 모두 넣겠습니다.(copy)
.map을 통해 새로운 배열을 만들어냅니다.
const [gameBoard, setGameBoard] = useState(initialGameBoard);
function handleSelectSquare(rowIndex, colIndex) {
setGameBoard((prevGameBoard)=> {
const updatedBoard = [...prevGameBoard.map(innerArray => [...innerArray])];
updatedBoard[rowIndex][colIndex] = 'X';
return updatedBoard;
});
}
상태 끌어올리기(Lifting State Up)
어떤 플레이어가 진행 중인지 해당 정보를 Player 컴포넌트, GameBoard 컴포넌트 둘 모두에게
속성(Porp)을 통해 보낼 수 있기 때문에 useState를 사용해서 상태를 App 컴포넌트에 추가하여
현재 진행 중인 플레이어를 여기에서 제어 하겠습니다.
const [activePlayer, setActivePlayer] = useState('X');
function handleSelectSquare(){
setActivePlayer((curActivePlayer)=> curActivePlayer === 'X' ? 'O' : 'X');
}
위의 선택된 내용은 당연히 GameBoard 컴포넌트에서 제어 됩니다.
return <main>
<div id="game-container">
<ol id="players" className="highlight-player">
<Player initalName="Player1" symbol="X" isActive={activePlayer === 'X'}/>
<Player initalName="Player2" symbol="O" isActive={activePlayer === 'O'}/>
</ol>
<GameBoard onSelectSquare={handleSelectSquare} activePlayerSymbol={activePlayer}/>
</div>
속성을 받아서 props 구조 분해 할당을 해서 onSelectSquare
export default function GameBoard({ onSelectSquare, activePlayerSymbol }) {
const [gameBoard, setGameBoard] = useState(initialGameBoard);
function handleSelectSquare(rowIndex, colIndex) {
setGameBoard((prevGameBoard)=> {
const updatedBoard = [...prevGameBoard.map(innerArray => [...innerArray])];
updatedBoard[rowIndex][colIndex] = activePlayerSymbol;
return updatedBoard;
});
onSelectSquare();
}
중복은 헬프 function을 만들어서 제거 합니다.
import Player from "./components/Player.jsx";
import GameBoard from "./components/GameBoard.jsx";
import {useState} from "react";
import Log from "./components/Log.jsx";
function deriveActivePlayer(gameTurns){
let currentPlayer = 'X';
if(gameTurns.length > 0 && gameTurns[0].player === 'X') {
currentPlayer = 'O';
}
return currentPlayer;
}
function App() {
const [gameTurns, setGameTurns] = useState([]);
// const [activePlayer, setActivePlayer] = useState('X');
const activePlayer = deriveActivePlayer(gameTurns);
function handleSelectSquare(rowIndex, colIndex) {
//setActivePlayer((curActivePlayer)=> curActivePlayer === 'X' ? 'O' : 'X');
setGameTurns((prevTurns) => {
const currentPlayer = deriveActivePlayer(prevTurns);
const updatedTurns = [
{ square: {row: rowIndex, col: colIndex }, player: currentPlayer },
...prevTurns,
];
return updatedTurns;
});
}
return <main>
<div id="game-container">
<ol id="players" className="highlight-player">
<Player initalName="Player1" symbol="X" isActive={activePlayer === 'X'}/>
<Player initalName="Player2" symbol="O" isActive={activePlayer === 'O'}/>
</ol>
<GameBoard onSelectSquare={handleSelectSquare}
turns={gameTurns}
/>
</div>
<Log turns={gameTurns}/>
</main>
}
export default App
React..이녀석 상태관리가 엄청 중요하다!!
재사용을 하려고 할 때도 그렇고
대충 어떤 느낌인지는 알겠다.
다음으로는 CSS Framework를 학습해보자!