리액트 앱은 컴포넌트로 구성됩니다. 컴포넌트는 고유의 논리와 형태를 갖는 하나의 UI입니다. 하나의 컴포넌트는 버튼처럼 작을 수도, 전체 페이지처럼 클 수도 있습니다.
리액트 컴포넌트는 마크업(Markup)을 반환하는 자바스크립트 함수입니다.
function MyButton() {
return (
<button>I'm a button</button>
);
}
이렇게 만든 MyButton
컴포넌트를 다음과 같이 또 다른 컴포넌트 안에 중첩시킬 수 있습니다.
export default function MyApp() {
return (
<div>
<h1>Welcome to my app</h1>
<MyButton />
</div>
);
}
대문자로 시작하는 관례는 그것이 리액트 컴포넌트임을 알려줍니다. HTML 태그는 소문자로 시작해야 하는 반면 리액트 컴포넌트는 대문자로 시작해야 합니다.
결과:
export default
키워드는 파일의 메인 컴포넌트를 명시합니다. 자바스크립트에 친숙하지 않다면 MDN, javascript.info를 참조하면 좋습니다.
위에서 봤던 마크업 문법을 JSX
라고 부릅니다. 필수는 아니지만, 대부분의 리액트 프로젝트들은 편의를 위해 JSX를 사용합니다. 로컬 개발을 위해 추천하는 툴은 모두 별도의 설정과 설치 없이도 JSX를 지원합니다.
JSX는 HTML보다 엄격합니다. <br />
같은 태그들은 closing-tag(/)가 필수적입니다. 컴포넌트는 여러 개의 JSX 태그들을 반환할 수도 없습니다. 여러 개의 JSX 태그들을 반환하고 싶다면 그것들을 부모 태그로 감싸야 합니다. 부모 태그는 <div>...</div>
혹은 Fragment(<>...</>)
등이 있습니다.
function AboutPage() {
return (
<>
<h1>About</h1>
<p>Hello there.<br />How do you do?</p>
</>
);
}
JSX로 변환해야 할 HTML이 너무 많다면, online converter를 활용할 수 있습니다.
리액트에서 CSS 클래스는 className
으로 명시합니다. className
은 HTML class
attribute와 같은 방식으로 동작합니다.
<img className="avatar" />
이제 CSS 규칙들을 분리된 CSS 파일에 작성합니다.
/* In your CSS */
.avatar {
border-radius: 50%;
}
리액트는 CSS 파일을 추가하는 방식을 규약하지 않습니다. 간단한 예시로, HTML에 <link>
태그를 추가해도 됩니다. 어떤 개발 도구나 프레임워크를 사용한다면, 문서를 참조하여 프로젝트에 CSS 파일을 추가하는 방법을 알아보세요.
JSX는 마크업을 자바스크립트에 포함시킬 수 있게 해줍니다. 마크업 내에서 자바스크립트 문법을 사용하려면 중괄호를 사용해야 합니다. 중괄호를 활용하여 코드에 변수를 포함시키고 사용자에게 데이터를 보여줄 수 있습니다. 예를 들어, 다음 코드는 user.name
을 보여줄 것입니다.
return (
<h1>
{user.name}
</h1>
);
JSX attributes에서도 자바스크립트 문법을 활용하기 위해 같은 방법을 사용합니다. Attributes 값에 따옴표("")를 사용하는 대신 중괄호를 사용해야 합니다. 예를 들어, 다음 코드는 className="avatar"
은 "avatar"
문자열을 CSS class로 전달하지만 src={user.imageUrl}
은 자바스크립트 문법으로 user.imageUrl
변수 값을 읽고, src
attribute로 그 값을 전달합니다.
return (
<img
className="avatar"
src={user.imageUrl}
/>
);
문자열 연결처럼 복잡한 표현식도 JSX 중괄호에 쓸 수 있습니다.
const user = {
name: 'Hedy Lamarr',
imageUrl: 'https://i.imgur.com/yXOvdOSs.jpg',
imageSize: 90,
};
export default function Profile() {
return (
<>
<h1>{user.name}</h1>
<img
className="avatar"
src={user.imageUrl}
alt={'Photo of ' + user.name}
style={{
width: user.imageSize,
height: user.imageSize
}}
/>
</>
);
}
위의 예시에서 style={{}}
은 독특한 문법은 아니고, 자바스크립트 객체 {}
가 JSX 중괄호style={ }
안에 있을 뿐입니다. 스타일이 자바스크립트 변수들에 의존할 때 style
attribute를 사용할 수 있습니다.
조건문을 작성하는 리액트의 독특한 문법은 없습니다. 일반 자바스크립트 코드를 쓰듯이 조건문을 사용합니다. 예를 들어, 조건 부로 JSX를 포함하기 위하여 if
문을 사용할 수 있습니다.
let content;
if (isLoggedIn) {
content = <AdminPanel />;
} else {
content = <LoginForm />;
}
return (
<div>
{content}
</div>
);
더 간결한 코드를 선호한다면 조건 연산자 ?
를 사용할 수 있습니다.
if
와 다르게 조건 연산자는 JSX 안에서도 동작합니다.
<div>
{isLoggedIn ? (
<AdminPanel />
) : (
<LoginForm />
)}
</div>
else
문이 필요없다면 더 짧게 논리 연산자 &&
도 사용할 수 있습니다.
<div>
{isLoggedIn && <AdminPanel />}
</div>
이 모든 접근 방식은 조건부로 attributes를 지정할 때도 작동합니다. 이러한 자바스크립트 문법에 익숙하지 않다면 항상 if...else문을 사용하여 시작할 수 있습니다.
컴포넌트의 배열들을 렌더링하기 위해 for
loop와 array map()
function 같은 자바스크립트 기능을 사용해야 합니다.
예를 들어, 다음과 같은 products 배열이 있다고 합시다.
const products = [
{ title: 'Cabbage', id: 1 },
{ title: 'Garlic', id: 2 },
{ title: 'Apple', id: 3 },
];
컴포넌트 내에서 map()
함수를 사용하여 products 배열을 <li>
항목의 배열로 변환합니다.
const listItems = products.map(product =>
<li key={product.id}>
{product.title}
</li>
);
return (
<ul>{listItems}</ul>
);
<li>
가 key
attribute를 갖는 방식에 주목하세요. 배열의 각 항목을 전달할 때, 다른 항목 사이에서 해당 항목을 고유하게 식별할 수 있는 문자열 또는 숫자를 key attribute로 같이 전달해야 합니다. 일반적으로 key는 데이터베이스 ID같은 데이터의 요소로 결정되어야 합니다. 리액트는 추후에 항목들을 삽입, 삭제, 정렬할 때 무슨 일이 일어났는 지 알기 위해 keys를 사용합니다.
const products = [
{ title: 'Cabbage', isFruit: false, id: 1 },
{ title: 'Garlic', isFruit: false, id: 2 },
{ title: 'Apple', isFruit: true, id: 3 },
];
export default function ShoppingList() {
const listItems = products.map(product =>
<li
key={product.id}
style={{
color: product.isFruit ? 'magenta' : 'darkgreen'
}}
>
{product.title}
</li>
);
return (
<ul>{listItems}</ul>
);
}
이벤트 핸들러 함수(event handler function)를 선언하여 컴포넌트 내에서 이벤트에 반응할 수 있습니다.
function MyButton() {
function handleClick() {
alert('You clicked me!');
}
return (
<button onClick={handleClick}>
Click me
</button>
);
}
onClick={handleClick}
에 소괄호 ()
가 없다는 점에 유의하세요! 이벤트 헨들러 함수를 호출하지 마세요. 함수 자체를 내려줘야 합니다. 리액트는 사용자가 버튼을 클릭할 때 이벤트 핸들러를 호출할 것입니다.
컴포넌트가 정보들을 "기억"하고 보여주기를 원하는 경우가 많습니다. 예를 들어 버튼이 클릭된 횟수를 세고 싶다면 컴포넌트에 상태(state)를 추가하세요.
1. import useState
from React
import { useState } from 'react';
이제 컴포넌트에 상태 변수(state variable)을 선언할 수 있습니다.
function MyButton() {
const [count, setCount] = useState(0);
// ...
useState
는 현재 상태 count
와 상태를 업데이트할 수 있는 함수 setCount
두 값을 반환합니다. 필수는 아니지만 변수명은 관례적으로 [something, setSomething]
형식으로 작성합니다.
버튼이 처음 보여질 때, useState()
로 0
을 전달했기 때문에 count
는 0
일 것입니다. 상태를 변경하려면 setCount()
를 호출하고 새 값을 상태에 전달해야 합니다. 버튼 클릭은 counter를 증가시킵니다.
function MyButton() {
const [count, setCount] = useState(0);
function handleClick() {
setCount(count + 1);
}
return (
<button onClick={handleClick}>
Clicked {count} times
</button>
);
}
리액트는 컴포넌트 함수를 다시 호출할 것입니다. 이 때, count
는 1
일 것입니다. 다음은 2
이고, 버튼을 누를 때마다 증가할 것입니다.
같은 컴포넌트를 여러 개 렌더링한다면 각 컴포넌트들은 고유의 상태를 갖습니다. 다음 코드를 실행하고 각 버튼을 개별적으로 클릭해 보세요.
import { useState } from 'react';
export default function MyApp() {
return (
<div>
<h1>Counters that update separately</h1>
<MyButton />
<MyButton />
</div>
);
}
function MyButton() {
const [count, setCount] = useState(0);
function handleClick() {
setCount(count + 1);
}
return (
<button onClick={handleClick}>
Clicked {count} times
</button>
);
}
각 버튼이 고유의 count
상태를 "기억하고" 다른 버튼들에 영향을 주지 않는다는 점에 유의하세요.
use
로 시작하는 함수를 훅(Hook)이라고 합니다. useState
는 리액트가 제공하는 내장 훅입니다. 다른 내장 훅들을 API reference에서 확인할 수 있습니다. 존재하는 훅들을 조합하여 커스텀 훅(custom hook)을 작성할 수도 있습니다.
훅 사용은 다른 함수보다 더 제한적입니다. 훅은 컴포넌트 상단에서만 호출할 수 있습니다. useState
를 조건이나 루프(loop) 안에서 사용하려면 새로운 컴포넌트를 만들어서 넣고, 그 컴포넌트 안에서 사용해야 합니다.
위의 예시에서 각각의 MyButton
은 고유의 독립적인 count
를 갖고, 각각의 버튼이 클릭될 때 클릭된 버튼의 count
만 변경되었습니다.
그러나 컴포넌트끼리 데이터를 공유하고 함께 업데이트될 필요가 있을 때가 많습니다.
두 MyButton
컴포넌트들이 같은 count
를 보여주고 함께 업데이트 되도록 하려면, 각각의 버튼에서 상태를 "위쪽"으로 이동시켜 모든 버튼을 포함하는 가장 가까운 컴포넌트에 상태를 저장해야 합니다.
MyApp
예시입니다.
이제 두 버튼 중 하나를 클릭하면, MyApp
안의 count
가 변경되어 MyButton
안의 두 count가 모두 변경됩니다. 이를 코드로 표현하는 방법은 다음과 같습니다.
먼저, state를 MyButton
에서 MyApp
으로 올립니다.
export default function MyApp() {
const [count, setCount] = useState(0);
function handleClick() {
setCount(count + 1);
}
return (
<div>
<h1>Counters that update separately</h1>
<MyButton />
<MyButton />
</div>
);
}
function MyButton() {
// ... we're moving code from here ...
}
그리고 나서, MyApp
으로부터 상태를 클릭 핸들러와 함께 각각의 MyButton
으로 내려보냅니다. 이전에 <img>
와 같은 내장 태그에서 했던 방식과 마찬가지로, JSX에서 중괄호를 사용하여 MyButton
으로 정보를 전달할 수 있습니다.
export default function MyApp() {
const [count, setCount] = useState(0);
function handleClick() {
setCount(count + 1);
}
return (
<div>
<h1>Counters that update together</h1>
<MyButton count={count} onClick={handleClick} />
<MyButton count={count} onClick={handleClick} />
</div>
);
}
이렇게 내려보내는 정보를 props
라고 합니다. 이제 MyApp
컴포넌트는 count
상태와 handleClick
이벤트 핸들러를 포함하고, 이를 각각의 버튼에 props로 전달합니다.
마지막으로 부모 컴포넌트에게 전달 받은 props를 읽기 위해 MyButton
을 변경합니다.
function MyButton({ count, onClick }) {
return (
<button onClick={onClick}>
Clicked {count} times
</button>
);
}
버튼을 클릭하면 onClick
핸들러가 실행됩니다. 각 버튼의 onClick
prop은 MyApp
내의 handleClick
함수로 설정되어 있으므로, handleClick
내부의 코드가 실행됩니다. 이 코드는 setCount(count + 1)
을 호출하여 count
상태 변수를 증가시킵니다. 새로운 count
값이 각 버튼에 prop으로 전달되어, 버튼들이 모두 새로운 값을 보여줍니다. 이를 lifting state up
이라고 합니다. 상태를 끌어올림으로써, 여러 컴포넌트 사이에서 상태를 공유하게 됩니다.
import { useState } from 'react';
export default function MyApp() {
const [count, setCount] = useState(0);
function handleClick() {
setCount(count + 1);
}
return (
<div>
<h1>Counters that update together</h1>
<MyButton count={count} onClick={handleClick} />
<MyButton count={count} onClick={handleClick} />
</div>
);
}
function MyButton({ count, onClick }) {
return (
<button onClick={onClick}>
Clicked {count} times
</button>
);
}
지금까지 React 코드를 작성하는 기본적인 방법에 대해 배웠습니다!
배운 내용을 실습에 적용하고 React로 첫 번째 미니 앱을 만들기 위해 Tutorial을 참고해 보세요.
Reference