index.html
앱의 기본 HTML템플릿이며, 대부분의 경우 변하지 않는다.
최상위 DOM요소(예 : <div id="root">)만 포함하고, 나머지 모든 UI는 리액트에서 동적으로 랜더링 된다.
index.js
ReactDOM을 사용해 리액트 컴포넌트를 index.html의 특정 요소에 랜더링한다. 보통 앱의 진입점(App.js)을 여기에 연결한다.
App.js
애플리케이션의 최상위 컴포넌트로, 전체 레이아웃을 구성한다.
주요 레이아웃 태그(예 : <header>, <main>, <section>, <footer>)를 정의 한다. 필요한 세부 컴포넌트를 가져와 조합한다.
개별 컴포넌트
UI요소를 작은 컴포넌트 단위로 나눈다.
예를들어,
Header : 상단 네비게이션 바
Footer : 하단 정보 표시
Button : 재사용 가능한 버튼
Card : 카드 UI 컴포넌트
npx create-react-app .을 해준뒤, 18.x.x버전이 아니라면
의존성 충돌이 생길 수 있기때문에 npm install react@^18.0.0 react-dom@^18.0.0이렇게
이렇게 프로젝트를 초기화 할 수 있다.
public폴더에선 index.html파일 이외에 전부 삭제해도 되며,
src폴더에선 index.js파일 이외에 전부 삭제해도 된다.
index.html : 웹 브라우저에서 가장 먼저 실행되는 파일
index.js : index.html파일이 열리고 나서 실행되는 파일. 리액트 코드들 중에서 가장 먼저 실행되는 파일이다.
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<h1>안녕 리액트</h1>
);
여기서 보면 root.render()가 있을텐데, render()라는것은 화면을 그린다는 뜻이다. 리액트에서는 이 render메소드로 HTML태그들을 만들어주는 것이다.
결과적으로 root라는 id를 가진 html요소가 아규먼트값으로 사용된다는것이다. 아규먼트인 root는 index.html파일 내에서 보면
<body>
<div id="root"></div>
</body>
여기 위의 div태그에 우리가 작성한 h1태그를 넣어준것이다.
이게 바로 리액트로 만든 HTML요소인것이다.
정리하면 일단 리액트 프로젝트가 실행되면 브라우저는 index.html파일을 연다.그리고 나서 index.js파일의 코드도 실행하는데, const root = ReactDOM.createRoot(document.getElementById('root'));여기에 가리키는 root DIV박스에 root.render(<h1>안녕 리액트</h1>);여기 render()안의 h1태그를 넣어주었다. 참고로 render()메소드는 index.js파일에서 한번만 실행하게 되기 때문에 억지로 외울 필요는 없다.
JSX는 자바스크립트와 HTML을 섞어서 쓸 수 있는 자바스크립트의 확장된 문법이라고 생각하면 된다. 위의
root.render(
<h1 id="title">안녕 리액트</h1>
);
과같은 문법이다.
하지만, HTML문법 모두를 사용할 순 없는데, 대표적으로 class와 for가 있다. class는 자바스크립트에서 사용하고 있기 때문에 JSX에서 class를 지정하고 싶다면 className을 사용해야 한다.
root.render(
<form>
<label className="ex-class" htmlFor="name">이름</label>
<input id="name" type="text" />
</form>
)
이런식으로 htmlFor와 className을 사용한다.
그리고 여러 단어로 조합된 속성명들은 모두 카멜케이스로 작성한다.
onBlur="",,
그리고 JSX요소들은 반드시 하나의 태그로 감싸주어야 한다.
예를들어
<p>hi</p>
<p>hello</p>
와 같은 태그는 쓸 수 없고
<div>
<p>hi</p>
<p>hello</p>
</div>
이렇게 하나의 태그로 감싸주어야 한다.
하지만 div태그를 굳이 만들고 싶지 않을때가 있다. 그럴때 사용하는것이 프래그먼트라는 것이다. <Fragment></Fragment>를 사용하면 불필요한 div태그를 줄일 수 있다.
<Fragment>
<p>hi</p>
</Fragment>
이렇게 써주면
<p>hi</p>
와 동일한 결과를 얻을 수 있다. 또, <></>이렇게 Fragment를 약식으로 쓸 수도 있다.
사실 JSX는 실제로 실행될때 자바스크립트 코드로 변환돼서 실행된다.
그래서 당연히 HTML코드 뿐만 아니라 자바스크립트 코드도 함께 작성할 수 있다.
const product = 'MacBook';
...
<>
<h1>나만의 {product.toUpperCase()}</h1>
</>
이렇게 중괄호를 열고 자바스크립트 문법을 사용할 수 있다.
이건 HTML속성에도 사용이 가능한데,
const product = 'MacBook';
const imgURL = 'imageURL';
...
<>
<h1>나만의 {product}</h1>
<img src={imgURL} alt="제품사진"/>
</>
이렇게 사용가능하다. 또 이벤트 핸들러도 마찬가지인데,
리액트에서는 이 JSX문법을 활용하기 때문에 이벤트 핸들러를 등록할때 addEventListener보다는 요소의 속성값으로 이벤트 핸들러를 등록하게 된다.
function handleClick () {
alert('도착');
}
...
<>
<button onClick={handleClick}>도착하기</button>
</>
JSX문법으로 작성한 요소는 결과적으로 자바스크립트 객체가 된다.
이러한 객체를 리액트 엘리먼트라고 부르는데, 이 리액트 엘리먼트를 리액트가 객체 형태의 값을 해석해서 html형태로 브라우저에 띄워주는것이다. 리액트 엘리먼트는 리액트로 화면을 그려내는데 가장 기본적인 요소이다.
리액트 컴포넌트는 리액트 엘리먼트를 좀 더 자유롭게 다루기 위한 하나의 문법이다. 컴포넌트를 만드는 가장 간단한 방법은 자바스크립트의 함수를 활용하는 것인데, 아래 코드에서 JSX문법으로 작성된 하나의 요소를 리턴하는 Hello함수가 바로 하나의 컴포넌트가 된다.
function Hello() {
return (
<h1>안녕 리액트</h1>
)
}
const element = (
<>
<Hello />
<Hello />
</>
)
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<>
element
</>
)
그리고 이렇게 컴포넌트를 작성하면, 위 코드에서 element변수 안의 JSX코드에서 볼 수 있듯, 컴포넌트 함수 이름을 통해 하나의 태그처럼 활용할 수 있다. 이런 특성은 모듈 문법으로 활용하면 더 독립적으로 컴포넌트 특성에 집중해서 코드를 짤 수 있다.
// Dice.js 파일
import diceBlue01 from './assets/dice-blue-1.svg';
function Dice() {
return <img src={diceBlue01} alt="주사위" />
}
export default Dice;
// App.js 파일
import Dice from './Dice.js';
function App() {
return (
<div>
<Dice />
</div>
)
}
export default App;
한가지 주의할 점은 리액트 컴포넌트의 이름은 반드시 첫글자를 대문자로 작성해야 한다는것이다. 컴포넌트의 이름이 첫글자가 소문자라면 오류가 난다.
JSX문법에서 컴포넌트를 작성할 때 컴포넌트에도 속성을 지정할 수 있다. 리액트에서 이렇게 컴포넌트에 지정한 속성들을 Props라고 부른다.
Props는 Propertis의 약자인데, 컴포넌트에 속성을 지정해주면 각 속성이 하나의 객체로 모여서 컴포넌트를 정의한 함수의 첫번째 파라미터로 전달된다.
// App.js
import Dice from './Dice.js';
function App() {
return (
<div>
<Dice color="blue" />
</div>
)
}
위 코드에서 <Dice color="blue" />color가 prop이 되는건데, 여기서 color는 html의 style속성의 color가 아닌 그냥 color라는 프로퍼티를 바인딩하기 위한 수단일 뿐이다.
// App.js
import Dice from './Dice.js';
function App() {
return (
<div>
<Dice color="blue" />
</div>
)
}
// Dice.js
import diceBlue01 from './assets/dice-blue-1.svg';
function Dice(props) {
console.log(props);
return <img src={diceBlue01} alt={props.color}/>
}
과 같이 사용할 수 있는데, 객체 디스트럭쳐링문법으로
function Dice({color}) {
console.log(color);
}
이렇게 간편하게 사용할 수 있다.
props에는 children이라는 특별한 프롭이 있다.
JSX문법으로 컴포넌트를 작성할 때 컴포넌트를 단일 태그가 아니라 여는 태그와 닫는 태그의 형태로 작성하면 그 안에 작성된 코드가 바로 이 children값에 담기게 된다.
// Button.js
function Button({children}) {
return <button>{childrend}</button>
}
// App.js
function App() {
return (
<div>
<Button onClick={...}>나는 버튼입니다.</Button>
</div>
)
}
저기의 "나는 버튼입니다."가 children이 되는것이다.
화면에 보여질 모습을 좀 더 직관적인 코드로 작성하고자 할 때 children값을 활용할 수 있다.
참고로 이 children을 활용하면 단순히 텍스트만 작성하는것을 넘어서 컴포넌트 안에 컴포넌트를 작성할 수도 있고, 컴포넌트안에 복잡한 태그들을 더 작성할 수 있다.
state는 리액트에서 화면을 그려내는데 굉장히 중요한 역할을 한다.
state라는 단어는 한국어로 상태라는 뜻인데, 리액트에서의 state도 그 의미가 다르지 않다. 상태가 바뀔때마다 화면을 새롭게 그려내는 방식으로 동작한다. 리액트에서 state를 만들고 state를 바꾸기 위해서는 일단 useState라는 함수를 활용해야 한다.
import {useState} from 'react';
...
const [num, setNum] = useState(1);
...
보통 이렇게 Dstructuring문법으로 작성하는데, useState함수가 초깃값을 아규먼트로 받고, 그에 따른 실행결과로 요소 2개를 가진 배열의 형태로 리턴하기 때문이다. 이때, 첫번째 요소가 바로 state이고, 두번째 요소가 이 기존의 state를 바꾸는 setter함수인데, 참고로 위 코드에서 볼수있듯, 첫번째 변수는 원하는 state의 이름(num)을 지어주고, 두번째 변수엔 state이름 앞에 set을 붙인 다음 카멜케이스로 이름(setNum)을 지어주는것이 일반적이다.
state는 변수에 새로운 값을 할당하는 방식으로 변경하는것이 아니라, 이 setter함수를 활용해야 하는데, setter함수는 호출할때 전달하는 아규먼트 값으로 state값을 변경해준다. 그래서 아래 코드처럼 setter함수를 활용해서 이벤트 핸들러를 등록해두면, 이벤트가 발생할 때 마다 상태가 변하면서 화면이 그려지는 것이다.
import {useState} from 'react';
import Button from './Button';
function App() {
const [num, setNum] = useState(1);
const handleRollClick = () => {
setNum(3); // 클릭시, num state를 3으로 변경
}
return (
<div>
<Button onClick={handleRollClick} />
</div>
)
}
export default App;
자바스크립트의 자료형은 크게 기본형과 참조형으로 나눌 수 있다.
특히 참조형 값들은 조금 독특한 특성을 가지고 있어서 변수로 다룰때도 주의해야 하는데, state를 활용할 때도 마찬가지 이다.
const [gameHistory, setGameHistory] = useState([]);
const handleRollClick() => {
const nextNum = random(6);
gameHistory.push(nextNum);
setGameHistory(gameHistory); // state가 제대로 변경 X
}
위 코드에서 볼 수 있듯, 배열값을 가진 gameHistory 에 push메소드를 이용해서 배열의 값을 변경한 다음, 변경된 배열을 setter함수로 state를 변경하려고 하면 코드가 제대로 동작하지 않는다.
gameHistory state는 배열 값 자체를 가지고 있는게 아니라 그 배열의 주솟값을 참조하고 있는건데, 때문에 push메소드로 배열안의 요소를 변경했다고 하더라도 결과적으로 참조하는 배열의 주솟값은 변경된것이 아니게 된다.
결과적으로 리액트 입장에서는 gameHistory state가 참조하는 주솟값은 여전히 똑같기 때문에 상태(state)가 바뀌었다고 판단하지 않는것이다.
그래서 참조형 state를 활용할때는 반드시 새로운 참조형 값을 만들어 state값을 변경해야 한다. 가장 간단한 방법으로는 Spread문법(...)을 활용하는 것이다.
const [gameHistory, setGameHistory] = useState([]);
const handleRollClick () => {
const nextNum = random(6);
setGameHistory([...gameHistory, nextNum]); // 제대로 변경
}
이 참초형 state의 특성을 이해하지 못하면 간혹 state가 제대로 변경되지 않는 버그가 발생했을때 원인을 제대로 찾지 못하는 경우가 발생할 수도 있다. 추가로 말해보자면
useState로 이전상태를 참조할땐 항상 콜백함수를 써주는것이 안전하다. 리액트에서 상태를 업데이트 할때 setState함수에 콜백함수를 사용하는 이유는 리액트의 상태 업데이트가 비동기적으로 동작하기 때문이다. 상태 업데이트가 바로 적용되지않고, 리액트는 여러 상태 업데이트를 병합해서 처리한다. 콜백함수를 사용하면 현재 상태값을 안전하게 참조하여 상태를 업데이트 할 수 있다.
setTotal((prevTotal) => prevTotal + 더할 값);
콜백함수의 첫번째 인자는 항상 이전 상탯값을 전달받는다. (예시에선 prevTotal)이전 상태값이 필요한 경우 항상 콜백함수를 활용하도록 하자.
이미지 파일은 import 구문을 통해 불러오고, 불러온 이미지 주소를 src속성으로 사용하면 된다.
import diceImg from './assets/imagePath';
function Dice() {
return <img src={diceImg}/>
}
export default Dice;
리액트에서 인라인 스타일은 문자열이 아닌 객체형으로 사용한다.
프로퍼티 이름은 css속성이름으로, 프로퍼티값은 css속성값으로 쓰는데, 이때 프로퍼티 이름은 대시 기호 없이 카멜케이스로 써야한다.
import 구문으로 파일을 불러올 수 있다.이때 from키워드 없이 쓰면 된다.
import ./Dice.css;
function Dice() {
return <img src...>
}
css파일에 정의된 클래스명을 className prop에 문자열로 넣어주면 된다. 이때 재사용성을 위해 className prop을 부모 컴포넌트에서 받으면 좋다.
import diceImg from './assets/imgPATH';
import ./Dice.css;
function Dice({className=''}) {
const classNames = `Dice${className}`;
return <img className={classNames} src={diceImg} alt="이미지">
}
클래스네임은 갯수가 많아질수록 복잡해지는데, 이렇게 쓰다보면 지저분해지고 매번 반복되는 코드를 작성한다는 번거로움이 생기는데 이럴때 classnames라이브러리를 사용해보자.