결국... 여기까지 왔는가...!
JavaScript, HTML, CSS 삼대장(?)을 물리치고 React 공부를 시작하게 되었습니다.
한 번에 알아야할 개념이 많아 처음에 상당히 어지러웠는데 드디어 정리를 해보려고 합니다.
리액트란 무엇일까? 공식문서를 살펴보도록 하겠습니다.
React는 사용자 인터페이스를 구축하기 위한 선언적이고 효율적이며 유연한 JavaScript 라이브러리입니다. “컴포넌트”라고 불리는 작고 고립된 코드의 파편을 이용하여 복잡한 UI를 구성하도록 돕습니다.
공부를 위해 vite 프레임워크로 빠르게 사용할 수 있는 리액트 프로젝트 환경을 세팅해 보겠습니다.
npm create vite@latest my-app -- --template react
cd my-app
npm install
위 명령어들을 순서대로 입력하시면 프로젝트가 생성됩니다. 타입스크립트를 사용하시고 싶은 경우엔 아래와 같이 입력하시면 됩니다. vite 공식 문서에 다양한 언어에 대한 지원 내용이 있으니 참고하시면 되겠습니다.
npm create vite@latest elice-todo-list -- --template react-ts
이제 프로젝트를 실행해봅시다.
npm run dev
터미널에 올라온 포트번호로 접속해 리액트 이미지가 보이면 성공입니다. vite를 사용하지 않을 시 create-react-app을 통해 리액트 앱을 생성할 수 있습니다.
리액트 기본 개념에 들어가기 전에 jsx에 대해 간단하게 짚고 넘어가겠습니다. 저희가 사용할 컴포넌트 코드는 jsx문법을 사용하면 가독성이 좋아지기 때문이죠! 바벨(babel)을 설치하면 사용하실 수 있고 위에 나온 개발환경 세팅을 따라하시거나 create-react-app을 통해 프로젝트를 생성했다면 기본적으로 포함되어 세팅되어 있습니다. 아래는 바벨 사용 전과 후 코드입니다.
//no babel
function Counter() {
const [count, setCount] = React.useState(0);
const handleButton = () => {
setCount(count + 1);
}
return React.createElement('div', null,
React.createElement('p', null, `카운트 - ${count}`),
React.createElement('button', { onClick: handleButton }, '증가')
);
}
//jsx
function Counter(){
const [count, setCount] = useState(0);
const handleButton = () => {
setCount(count+1);
}
return (
<div>
<p> 카운트 - {count}</p>
<button onClick={handleButton}>증가</button>
</div>
)
}
훨씬 가독성이 좋은 것을 확인하실 수 있습니다.
컴포넌트는 리액트의 기본 구성 요소로 재사용 가능한 코드 블록입니다. UI의 일부분을 표현하며 독립적으로 관리됩니다. 예를들어 버튼, 헤더, 사이드바, 목록 등 모든 것이 컴포넌트로 표현될 수 있죠. 리액트 컴포넌트는 클래스형과 함수형 크게 두 가지로 나눌 수 있습니다.
요즘엔 함수형 컴포넌트를 주로 사용하기 때문에 잘 보이지 않아 생김새만 보고 넘어가셔도 될 것 같습니다. React.Component를 상속받아 render()를 통해 렌더링될 컴포넌트를 리턴합니다.
class Welcome extends React.Component {
render() {
return <h1>Hello, {this.props.name}</h1>;
}
}
말 그대로 함수로 컴포넌트를 정의합니다. 간단하고 가독성이 좋습니다. 함수에서 그려질 컴포넌트를 리턴합니다. 리턴시 항상 하나의 상위 태그로 감싸져 있어야 합니다.
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
{/*return (<h1>Hello, {props.name}</h1>
<h2>Hello, {props.name}</h2>) 상위 태그가 하나가 아니라 에러*/}
}
리액트는 브라우저의 실제 DOM과 동일한 구조의 가상 DOM을 유지합니다. UI가 업데이트되면 가상DOM에서 처리 후 실제 DOM과 비교go 변경된 부분만 업데이트합니다. 실제 DOM을 수정하는건 무거운 작업이기 때문에 이런 방식으로 빠른 UI업데이트가 가능하게 되지요.
리액트에서 컴포넌트 데이터를 처리하교 표시하는 데 두 가지 주요 개념을 사용합니다.
useState훅을 사용해서 쉽게 상태를 만들 수 있습니다. 간단한 카운터 예제를 살펴보겠습니다.//App.jsx
import React, {useState} from 'react'
function Counter(){
const [count, setCount] = useState(0);
const handleButton = () => {
setCount(count+1);
}
return (
<div>
{/*중괄호를 통해 js에서 사용되는 변수, 함수 등을 사용할 수 있습니다*/}
<p> 카운트 - {count}</p>
<button onClick={handleButton}>증가</button>
</div>
)
}
export default function App() {
return (
<div>
<Counter />
</div>
);
}
App 함수에서 <Counter name={testName} /> 부분 Counter 컴포넌트에서 props를 념기고 있는 부분을 확인해주세요. //App.jsx
import React, {useState} from 'react'
/*Counter(props){
props.testName
이런식으로 사용해도 된다.
*/
function Counter({name}){ //props 받음
const [count, setCount] = useState(0);
const handleButton = () => {
setCount(count+1);
}
return (
<div>
<p>{name} 카운트 - {count}</p>
<button onClick={handleButton}>증가</button>
</div>
)
}
export default function App() {
const testName = "파인애플피자";
return (
<div>
{/*props 전달*/}
<Counter name={testName} />
</div>
);
}
상태값은 사용되는 컴포넌트 내에서 수정이 가능하지만 부모로부터 받은 속성값(props)은 해당 부모에서 수정해야합니다.
함수형 컴포넌트에서는 hook을 사용해 컴포넌트 상태 관리와 라이프사이클 이벤트를 처리할 수 있습니다. 3가지 주요 훅에 대해 알아보겠습니다.
함수형 컴포넌트에서 상태를 관리하기 위한 훅입니다. 상태를 선언하고 업데이트할 수 있습니다. 상태 변경시 해당 컴포넌트는 리렌더링됩니다.
function Counter(){
//useState를 통해 count 상태과 그 상태를 업데이트하는 함수를 선언
const [count, setCount] = useState(0); //0으로 초기화
const handleButton = () => {
setCount(count+1);
}
return (
<div>
{/*중괄호를 통해 js에서 사용되는 변수, 함수 등을 사용할 수 있습니다*/}
<p> 카운트 - {count}</p>
<button onClick={handleButton}>증가</button>
</div>
)
}
컴포넌트에서 사이드 이펙트를 다루기 위한 훅입니다. 컴포넌트의 라이프 사이클 이벤트(마운트, 언마운트, 업데이트)에서 처리되는 작업을 수행할 수 있습니다.
import React, { useState, useEffect } from "react";
function Timer() {
const [seconds, setSeconds] = useState(0);
// useEffect를 사용하여 타이머 설정
useEffect(() => {
const timer = setInterval(() => {
setSeconds((prevSeconds) => prevSeconds + 1);
}, 1000);
// 컴포넌트가 언마운트되면 타이머 정리
return () => clearInterval(timer);
}, []);
return <div>Timer: {seconds} seconds</div>;
}
useEffect(callback, [])두 번째 인자를 의존성 배열이라고 부릅니다. []가 들어가면 렌더링 결과가 돔에 반영된 후 호출됩니다. [count]이런 식으로 상태를 주면 그 인자가 업데이트될 때 처리를 할 수 있도록 합니다. 배열이기 때문에 여러개를 넣을 수도 있습니다. 하지만 대부분의 경우 버그가 많이 생겨 빈 배열로 사용합니다.
리액트 컴포넌트 트리 안에서 데이터를 전역적으로 공유할 수 있게 해주는 훅입니다. 이를 통해 props를 여러 계층에 걸쳐 전달하는 props drilling이 일어나지 않게 컴포넌트간 데이터를 공유할 수 있습니다.
props drilling : 리액트에서 컴포넌트간 데이터 전달시 하위 컴포넌트로 전달하기 위해 중간 컴포넌트를 통해 내려주는 것입니다. 자식에게 전달하기 위해 필요하지만 직접 사용하지 않는 컴포넌트까지 프로퍼티를 받고 전달해야 합니다.
먼저 createContext를 통해 컨텍스트를 생성하고 Provider컴포넌트로 컨텍스트 값을 정의합니다. 사용할 땐 useContext훅을 통해 해당 값을 사용할 수 있습니다.
import React, { createContext, useContext } from 'react';
// 컨텍스트 생성
const MyContext = createContext();
function App() {
return (
// Provider를 사용하여 컨텍스트 값 설정
<MyContext.Provider value="Hello, Context!">
<Child />
</MyContext.Provider>
);
}
function Child() {
// useContext를 사용하여 컨텍스트 값 접근
const contextValue = useContext(MyContext);
return <div>{contextValue}</div>;
}
컴포넌트 트리 깊숙한 곳에 있는 컴포넌트들이 props를 전달받지 않고도 필요한 데이터에 접근할 수 있습니다. 또한 필요한 부분만 변화되기 때문에 불필요한 렌더링도 방지할 수 있죠!
하지만 전역으로 공유할 데이터가 여러개가 되면 어떻게 해야할까요? 계속해서 Provider로 감싸줘야하고 그건 가독성 별로고 사용하기도 불편합니다. 때문에 보통 상태 관리 라이브러리를 따로 사용합니다. 많이 사용되는건 redux, recoil, jotai, react-query, zustand 등이 있습니다.
리액트의 이벤트 처리는 HTML이벤트 처리와 비슷하지만 차이점이 있습니다. 리액트에서는 이벤트 이름을 카멜 케이스로 작성하고 이벤트를 처리하는 함수를 문자열이 아닌 함수 형태로 전달합니다.
function MyComponent() {
const handleClick = () => {
console.log('버튼이 클릭되었습니다.');
};
return (
<button onClick={handleClick}>클릭</button>
);
}
위의 버튼 태그를 살펴보면 onClick이 카멜 케이스로 되어있고 함수를 넘겨주는 것을 확인하실 수 있습니다.
리액트의 함수형 컴포넌트 내에서 상태 관리를 위애 useState훅을 사용합니다. 위에서 훅에 대해 배울때 이미 봤었죠? 이 훅을 사용해 상태와 상태를 업데이트 하는 함수를 제공받습니다. 하지만 아래 코드를 보시면 setCount를 호출하는 부분이 조금 다릅니다. setCount(prevCount => prevCount + 1)이런 식으로 이전 상태값을 기반으로 새로운 상태를 계산할 수 있도록 업데이트할 수도 있습니다.
import React, { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
const handleIncrement = () => {
setCount(prevCount => prevCount + 1)
};
return (
<div>
<p>현재 카운트: {count}</p>
<button onClick={handleIncrement}>증가</button>
</div>
);
}
이 코드에서 useState(0)은 count라는 상태 변수를 선언하며, 초기값으로 0을 할당합니다. setCount 함수를 사용하여 count 값을 업데이트할 수 있습니다.
상태가 업데이트되면 해당 컴포넌트는 자동으로! 다시 렌더링됩니다. 리액트에선 상태 변경을 감지하고 UI를 최신 상태로 유지하기 위해 컴포넌트를 업데이트 하기 때문이죠! 위의 가상돔에서 다루었던 내용이지만 중요하니까 한 번 더 정리하겠습니다.
리액트에서 페이지 라우팅을 관리하는 라이브러리입니다. 이 라이브러리를 통해 SPA에서 사용자가 여러 페이지를 이동하는 것 처럼 느끼게 할 수 있습니다.
SPA : Single Page Application의 약자로 서버로부터는 하나의 페이지를 받아 필요한 컨텐츠를 동적으로 렌더링합니다. 필요한 데이터만 서버에서 불러와 일부를 업데이트하기 때문에 사용자는 페이지 사용이 굉장히 빠르고 부드럽다고 느끼게 됩니다. 하지만 코드가 커지면 시작 속도가 느려질 수 있고 SEO문제가 발생할 수 있습니다. 이러한 부분은 SSR(서버 사이드 렌더링)이나 SSG(정적 사이트 생성)같은 솔루션으로 해결할 수 있습니다.
라이브러리이기 때문에 우선 설치해주도록 합시다.
npm install react-router-dom
아래의 요소들을 통해 라우팅을 할 수 있습니다.
아래 예시를 통해 사용법을 알아봅시다.
import { BrowserRouter, Routes, Route, Link } from 'react-router-dom';
function App() {
return (
<BrowserRouter>
<div>
<nav>
<Link to="/">홈</Link>
<Link to="/about">소개</Link>
</nav>
<Routes>
<Route path="/" exact element={<Home />} />
<Route path="/about" element={<About />} />
</Routes>
</div>
</BrowserRouter>
);
}
function Home() {
return <h2>홈 페이지</h2>;
}
function About() {
return <h2>소개 페이지</h2>;
}
NavLink는 현재 경로와 일치할 때 스타일을 변경할 수 있는 Link의 특별한 버전입니다. 라우트 파라미터를 사용하여 동적인 페이지 라우팅도 가능합니다.
<Route path="/user/:id" element={User} />
function User({ match }) {
return <h2>사용자 ID: {match.params.id}</h2>;
}
중첩 라우팅을 지원하여 하위 경루 내에서 더 세부적인 라우팅을 구현할 수 있습니다. 특정 조건에 따라 다른 경로로 리다이렉트도 가능합니다.
다음엔 실제로 어떻게 써야하는지 더 자세히 다루는 글로 돌아오겠습니다.
실전 리액트 프로그래밍(이재승) - 인사이트
https://ko.reactjs.org/
https://velog.io/@hongduhyeon/React-SPA