리액트 빠르게 시작해 보기
- Published on
- 이 페이지에서는 매일 사용할 80%의 리액트 개념에 대해 소개해 드립니다.
Table of Contents
주요 내용 소개
이 페이지는 리액트 시작을 위한 깊이 있는 안내서를 찾을 수 있으며, 일상적으로 마주치게 되는 모든 기본 개념을 다룹니다. 몇 가지 주요한 포인트를 요약해 보겠습니다.
컴포넌트 생성 및 중첩 방법
리액트 앱은 크거나 작은 컴포넌트로 구성되며, 이 컴포넌트는 JSX 문법을 사용해 마크업을 반환합니다.
마크업 및 스타일 추가하기
리액트는 JSX 마크업 구문을 사용합니다. JSX는 모든 태그를 닫아야 하며, className
으로 CSS 클래스를 지정합니다.
데이터 표시하기
리액트에서는 JSX 내에 중괄호를 사용해 JavaScript 표현식을 삽입하며, 이를 통해 동적 데이터를 표시합니다.
조건 렌더링하기
리액트에는 조건부 렌더링을 위한 별도의 구문이 없습니다. 대신, JavaScript의 if-else
나 삼항 연산자로 컴포넌트나 요소의 렌더링을 결정합니다.
목록 렌더링하기
리액트는 데이터 배열을 매핑해 JSX 요소 목록을 생성하며, key
속성으로 각 요소를 고유하게 식별합니다.
이벤트 응답하기
리액트는 사용자 이벤트에 응답하기 위해 컴포넌트에서 이벤트 핸들러 함수를 정의합니다.
화면 업데이트하기
리액트의 useState
훅으로 컴포넌트의 로컬 상태를 관리하며, 상태 변경 시 컴포넌트가 다시 렌더링됩니다.
컴포넌트 간에 데이터 공유하기
공통 데이터를 여러 컴포넌트에서 공유하려면, 상태를 최근 공통 조상으로 올리고, 이를 자식에 props
로 전달합니다.
이것은 시작에 불과하며, 리액트에는 Context, Ref, 라이프사이클 메서드, 고차 컴포넌트, 렌더 Props 등의 고급 기능과 개념이 더 있으며, 이러한 내용을 공식 문서에서 자세히 알아볼 수 있습니다.
컴포넌트 생성 및 중첩
리액트 앱은 컴포넌트로 구성됩니다. 컴포넌트는 컴포넌트라는 개체로 구성됩니다. 컴포넌트는 JavaScript 클래스나 함수로 이루어져 있으며, 자체적인 로직과 외관을 가진 UI(사용자 인터페이스)의 한 부분입니다. 컴포넌트는 버튼과 같이 작을 수도 있고 전체 페이지와 같이 클 수도 있습니다.
리액트 컴포넌트는 마크업을 반환하는 JavaScript 함수입니다.
function MyButton() {
return (
<button>I'm a button</button>
);
}
이제 MyButton
을 다른 컴포넌트에 중첩할 수 있습니다.
export default function MyApp() {
return (
<div>
<h1>내 앱에 오신 것을 환영합니다</h1>
<MyButton />
</div>
);
}
<MyButton />
이 대문자로 시작함에 유의하세요. 이렇게 하면 리액트 컴포넌트라는 것을 알 수 있습니다. 리액트 컴포넌트 이름은 항상 대문자로 시작해야 하며, HTML 태그는 소문자여야 합니다.
export default
키워드는 파일에서 주요 컴포넌트를 지정합니다. JavaScript 구문에 익숙하지 않은 경우, MDN과 javascript.info에서 훌륭한 참조 자료를 제공합니다.
JSX를 사용한 마크업 작성
위에서 보았던 마크업 구문을 JSX라고 합니다. 이는 선택적이지만, 대부분의 리액트 프로젝트는 편의성을 위해 JSX를 사용합니다. 로컬 개발을 위해 권장하는 모든 도구는 JSX를 기본적으로 지원합니다.
JSX는 HTML보다 엄격합니다. <br />
과 같이 태그를 닫아야 합니다. 컴포넌트는 또한 여러 개의 JSX 태그를 반환할 수 없습니다. 이를 공유 부모(<div>
...</div>
또는 빈 < >
...< / >
래퍼)로 묶어야 합니다.
function AboutPage() {
return (
<>
<h1>About</h1>
<p>Hello there.<br />How do you do?</p>
</>
);
}
JSX로 변환할 HTML이 많은 경우, 온라인 변환 도구(HTML to JSX 변환 도구)를 사용할 수 있습니다.
스타일 추가
리액트에서는 className으로 CSS 클래스를 지정합니다. 이는 HTML의 class 속성과 동일한 방식으로 작동합니다.
<img className="avatar" />
그런 다음 별도의 CSS 파일에서 해당 클래스에 대한 CSS 규칙을 작성합니다.
/* CSS 파일에 작성 */
.avatar {
border-radius: 50%;
}
리액트는 CSS 파일을 추가하는 방법을 명시하지 않습니다. 가장 간단한 경우, HTML에 <link>
태그를 추가합니다. 빌드 도구나 프레임워크를 사용하는 경우, 해당 문서를 참조하여 프로젝트에 CSS 파일을 추가하는 방법을 알아보세요.
리액트 프로젝트에 CSS 파일을 추가하는 방법
리액트 프로젝트에 CSS 파일을 추가하는 방법은 몇 가지 단계로 수행할 수 있습니다.
먼저, 스타일을 적용하려는 리액트 컴포넌트와 동일한 디렉토리에 CSS 파일을 생성합니다. 컴포넌트 파일의 이름이
MyComponent.js
라면, CSS 파일의 이름을MyComponent.css
로 지정할 수 있습니다.다음으로, 리액트 컴포넌트 파일(
MyComponent.js
)에서 파일 상단에 CSS 파일을 import합니다. 이는import
키워드와 CSS 파일의 경로를 사용하여 수행됩니다.
다음은 예시입니다.
다음과 같은 프로젝트 구조를 가정해 봅시다.
/my-app
/src
/components
MyComponent.js
MyComponent.css
그리고 MyComponent.css
파일에 몇 가지 스타일이 포함되어 있다고 가정해 봅시다.
.container {
margin: auto;
width: 50%;
border: 3px solid green;
padding: 10px;
}
그럼, MyComponent.js
파일에서 이 CSS 파일을 다음과 같이 import합니다.
import React from 'react';
import './MyComponent.css'; // CSS를 import합니다.
function MyComponent() {
return (
<div className="container">
<h1>Hello, World!</h1>
</div>
);
}
export default MyComponent;
이렇게 하면 MyComponent
컴포넌트의 div에 있는 .container
클래스 스타일이 MyComponent.css
에서 적용됩니다.
참고로, 이 방법은 Create-React-App, Next.js 등과 같은 프레임워크에서 기본적으로 작동합니다. 이러한 프레임워크는 CSS를 처리하는 webpack 로더가 이미 구성되어 있습니다. 하지만 프로젝트를 처음부터 설정하거나 다른 환경에서 작업하는 경우에는 css-loader
와 style-loader
와 같은 로더를 구성해야 할 수도 있습니다. 또는 styled-components와 같은 CSS-in-JS 라이브러리를 사용할 수도 있습니다.
데이터 표시
JSX를 사용하면 JavaScript 내에 마크업을 넣을 수 있습니다. 중괄호를 사용하여 코드의 변수를 포함하여 사용자에게 표시할 수 있습니다. 예를 들어, 다음은 user.name
을 표시합니다.
return (
<h1>
{user.name}
</h1>
);
JSX 속성에서도 중괄호를 사용하여 JavaScript로 "이스케이프"할 수 있지만, 따옴표 대신 중괄호를 사용해야 합니다. 예를 들어, className="avatar"
는 "avatar"
문자열을 CSS 클래스로 전달하지만, src={user.imageUrl}
은 JavaScript의 user.imageUrl
변수 값을 읽고 해당 값을 src
속성으로 전달합니다.
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={{}}
는 특별한 구문이 아니라, style={ }
JSX 중괄호 내부의 일반 {}
객체입니다. 스타일이 JavaScript 변수에 따라 달라지는 경우 style
속성을 사용할 수 있습니다.
조건부 렌더링
리액트에서 조건을 작성하는 특별한 구문은 없습니다. 대신 일반적인 JavaScript 코드를 작성할 때 사용하는 기술을 사용합니다. 예를 들어, 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>
이러한 접근 방식은 속성을 조건부로 지정하는 데에도 동일하게 적용됩니다. JavaScript 구문에 익숙하지 않은 경우, 항상 if...else
를 사용하는 것으로 시작할 수 있습니다.
목록 렌더링
구성 요소의 목록을 렌더링하기 위해 for
루프와 배열의 map()
함수와 같은 JavaScript 기능을 활용할 것입니다.
예를 들어, 제품의 배열이 있다고 가정해봅시다.
const products = [
{ title: '배추', id: 1 },
{ title: '마늘', id: 2 },
{ title: '사과', id: 3 },
];
컴포넌트 내부에서 map()
함수를 사용하여 제품 배열을 <li>
항목의 배열로 변환합니다.
const listItems = products.map(product =>
<li key={product.id}>
{product.title}
</li>
);
return (
<ul>{listItems}</ul>
);
<li>
에는 key 속성이 있음에 유의하세요. 목록의 각 항목마다 형제들 중에서 해당 항목을 고유하게 식별하는 문자열 또는 숫자를 전달해야 합니다. 일반적으로 키는 데이터로부터 가져온 데이터베이스 ID와 같은 값이어야 합니다. 리액트는 키를 사용하여 항목을 나중에 삽입, 삭제 또는 재정렬할 때 어떤 일이 발생했는지 알 수 있습니다.
이벤트에 응답하기
컴포넌트 내부에서 이벤트 핸들러 함수를 선언함으로써 이벤트에 응답할 수 있습니다.
function MyButton() {
function handleClick() {
alert('You clicked me!');
}
return (
<button onClick={handleClick}>
Click me
</button>
);
}
onClick={handleClick}
에서 마지막에 괄호가 없음에 주목하세요! 이벤트 핸들러 함수를 호출하지 마세요. 함수를 전달하기만 하면 됩니다. 리액트는 사용자가 버튼을 클릭할 때 이벤트 핸들러를 호출합니다.
화면 업데이트
컴포넌트에서 정보를 "기억"하고 표시하려는 경우가 자주 있습니다. 예를 들어, 버튼이 클릭된 횟수를 세고 싶을 수 있습니다. 이를 위해 컴포넌트에 상태(state)를 추가하세요.
먼저, 리액트에서 useState
를 import하세요.
import { useState } from 'react';
이제 컴포넌트 내부에 상태 변수를 선언할 수 있습니다.
function MyButton() {
const [count, setCount] = useState(0);
// ...
useState
에서 두 가지를 얻게 됩니다. 현재 상태(count
)와 상태를 업데이트할 수 있는 함수(setCount
). 이들에게 아무 이름이나 지정할 수 있지만, [something, setSomething]
형식으로 작성하는 것이 관례입니다.
버튼이 처음 표시될 때 count
는 0이 됩니다. 왜냐하면 useState()
에 0을 전달했기 때문입니다. 상태를 변경하려면 setCount()
를 호출하고 새 값을 전달하면 됩니다. 이 버튼을 클릭하면 카운터가 증가합니다.
function MyButton() {
const [count, setCount] = useState(0);
function handleClick() {
setCount(count + 1);
}
return (
<button onClick={handleClick}>
Clicked {count} times
</button>
);
}
리액트는 컴포넌트 함수를 다시 호출합니다. 이번에는 count
가 1이 될 것입니다. 그 다음에는 2가 될 것입니다. 그리고 이런 식으로 계속됩니다.
동일한 컴포넌트를 여러 번 렌더링하면 각각 자체 상태를 가지게 됩니다. 각 버튼을 개별적으로 클릭해보세요.
각 버튼이 자체적인 count
상태를 "기억"하고 다른 버튼에 영향을 주지 않는 것을 알아차리세요.
Hooks 사용
use
로 시작하는 함수를 Hooks
라고 합니다. useState
는 리액트에서 제공하는 내장 Hook입니다. API 참조에서 다른 내장 Hooks도 찾을 수 있습니다. 또한 기존 Hooks를 조합하여 직접 Hooks를 작성할 수도 있습니다.
Hooks는 다른 함수보다 더 제한적입니다. 컴포넌트의 상단(또는 다른 Hooks)에서만 Hooks를 호출할 수 있습니다. 조건문이나 반복문에서 useState
를 사용하려면 새로운 컴포넌트를 추출하고 그곳에 두세요.
컴포넌트 간 데이터 공유
이전 예제에서 각 MyButton
은 독립적인 count
를 가지고 있으며 각 버튼이 클릭될 때 클릭한 버튼의 count
만 변경됩니다.
처음에는 각 MyButton의 count 상태가 0입니다.
첫 번째 MyButton은 count를 1로 업데이트합니다.
그러나 종종 컴포넌트들이 데이터를 공유하고 항상 함께 업데이트해야 할 필요가 있습니다.
두 개의 MyButton
컴포넌트가 동일한 count
를 표시하고 함께 업데이트하려면, 개별 버튼에서 상태를 "상위로" 이동하여 모두를 포함하는 가장 가까운 컴포넌트인 MyApp
으로 이동해야 합니다.
이 예시에서는 MyApp입니다.
처음에는 MyApp
의 count
상태는 0이며, 이는 두 개의 자식 컴포넌트에 전달됩니다.
클릭 이벤트 발생 시, MyApp
은 자체적인 count
상태를 1로 업데이트하고 이를 두 개의 자식 컴포넌트에 전달합니다.
이제 버튼 중 하나를 클릭하면 MyApp
의 count
가 변경되고, 이는 MyButton
의 두 개의 count
도 변경시킵니다. 아래는 이를 코드로 표현한 방법입니다.
먼저, MyButton
에서 상태를 MyApp
으로 이동합니다.
export default function MyApp() {
const [count, setCount] = useState(0);
function handleClick() {
setCount(count + 1);
}
return (
<div>
<h1>개별적으로 업데이트되는 카운터</h1>
<MyButton />
<MyButton />
</div>
);
}
function MyButton() {
// ... 여기서 코드를 이동합니다 ...
}
그런 다음, MyApp
에서 상태를 각 MyButton
으로 전달하고 공유된 클릭 핸들러와 함께 전달합니다. JSX 중괄호를 사용하여 MyButton
으로 정보를 전달할 수 있습니다. 이전에 <img>
와 같은 내장 태그에 사용한 것과 같은 방식입니다.
export default function MyApp() {
const [count, setCount] = useState(0);
function handleClick() {
setCount(count + 1);
}
return (
<div>
<h1>함께 업데이트되는 카운터</h1>
<MyButton count={count} onClick={handleClick} />
<MyButton count={count} onClick={handleClick} />
</div>
);
}
이렇게 전달하는 정보를 props
라고 합니다. 이제 MyApp
컴포넌트에는 count
상태와 handleClick
이벤트 핸들러가 포함되어 있으며, 두 가지 모두를 각 버튼으로 props
로 전달합니다.
마지막으로, MyButton
을 수정하여 부모 컴포넌트에서 전달된 props
를 읽도록 변경합니다.
function MyButton({ count, onClick }) {
return (
<button onClick={onClick}>
{count}번 클릭됨
</button>
);
}
버튼을 클릭하면 onClick
핸들러가 실행됩니다. 각 버튼의 onClick
prop
은 MyApp
내부에서 handleClick
함수로 설정되었으므로 해당 함수 내의 코드가 실행됩니다. 해당 코드는 setCount(count + 1)
을 호출하여 count
상태 변수를 증가시킵니다. 새로운 count
값은 각 버튼에 prop
로 전달되므로 새로운 값이 모두 표시됩니다.
이를 "상태를 위로 끌어올리기(lifting state up)"라고 합니다. 상태를 위로 이동함으로써 여러 컴포넌트 간에 상태를 공유할 수 있게 되었습니다.
Footnotes
"바인딩(binding)"이라는 용어는, 프로그래밍 컨텍스트에서 특정 객체나 값과 함수나 변수를 연결하는 과정을 의미합니다. JavaScript의 객체 지향 프로그래밍에서, 특히 클래스와 관련된 메서드에서 this 키워드의 값은 해당 메서드가 호출되는 방식에 따라 다를 수 있습니다. JavaScript 클래스의 메서드에 대해 이야기할 때, "바인딩되지 않았다"는 것은, 해당 메서드가 호출될 때
this
의 값이 그 클래스의 인스턴스를 자동으로 가리키지 않는다는 의미입니다. ↩