요 며칠간 React에서 사용하는 문법과 주요기능에 대해서 전반적으로 공부한 내용을 정리해 보았습니다. 아래 글 내용은 React+Next.js를 기준입니다.의외로 직관적이어서 어렵지 않다는 느낌을 받았는데 실제 애플리케이션을 만들면 또 다른느낌이겠죠.
React는 Facebook에서 만든 사용자 인터페이스를 구축하기 위한 JavaScript 라이브러리입니다.
컴포넌트 기반,가상 DOM, 양방향 데이터 바인딩, Hooks 등의 특징이 있습니다.
Next.js는 React 기반의 서버 사이드 렌더링(SSR) 및 정적 사이트 생성(SSG) 프레임워크입니다.
서버 사이드 렌더링 (SSR), 정적 사이트 생성 (SSG), 파일 시스템 기반 라우팅, API 라우트 와 같은 기능을 제공합니다.
결론은,
React는 사용자 인터페이스를 컴포넌트 기반으로 구성하고 Next.js는 서버 사이드 렌더링 및 정적 사이트 생성 기능을 활용하여 개발하는 것이 효율적입니다.
.js(JavaScript), .jsx(JavaScript XML), .tsx(TypeScript JSX) 어느걸 써야할까?
결론은 tsx가 아닐까 합니다. TypeScript+JSX. TypeScript로 작성된 React 컴포넌트에 사용되는 확장자이므로 TypeScript의 타입 체크 기능과 JSX의 컴포넌트 정의 기능을 모두 사용할 수 있으니까요.
굳이 타입스크립트를 배제할 이유가 없군요.
src/
├── components/ # 재사용 가능한 UI 컴포넌트
├── pages/ # 각 페이지 별 컴포넌트 (Next.js에서는 라우팅용)
├── utils/ # 유틸리티 함수들
├── hooks/ # custom hooks
├── contexts/ # React context (상태 관리)
├── public/ # 정적 파일들 (이미지, 폰트 등)
├── styles/ # 공통 스타일, CSS 모듈, SCSS 등
└── index.js or index.tsx # 진입점
{
"compilerOptions": {
"baseUrl": "./src",
"paths": {
"@components/*": ["components/*"]
}
}
}
위의 설정을 통해 아래와 같이 절대 경로로 컴포넌트를 임포트 할 수 있습니다.
import Button from '@components/Button';
src/pages/
하위에 파일을 만드는것만으로도 화면라우팅이 자동으로 이루어집니다. Next.js덕이죠.
화면단위의 파일(에를들어 index.tsx)은 컴포넌트로 이루어져 있는데 화면에 UI를 최소단위로 분할하여 src/component/
하위폴더에 파일을 작성하면됩니다.
아래 설명이 나오지만 컴포넌트별로 css를 지정할 수 있으므로 최종적으로는 컴포넌트폴더가 만들어지겟네요.
예를들어 button이라는 컴포넌트를 만든다면 이렇게 되겠죠.
src/component/button/button.module.css
src/component/button/button.tsx
파일명이나 컴포넌트의 네이밍은 일반적으로 아래와 같은 기준으로 지정하면 됩니다.
컴포넌트 이름
PascalCase를 사용합니다. (첫 글자를 대문자로 시작)
예: UserProfile, SidebarNavigation
파일 이름
컴포넌트의 파일 이름도 해당 컴포넌트의 이름과 일치하도록 PascalCase를 사용합니다.
예: UserProfile.jsx
, SidebarNavigation.tsx
프로퍼티와 상태 변수
camelCase를 사용합니다.
예: isLoading
, userProfile
, handleClick
이벤트 핸들러
handle
또는 on
접두사와 함께 camelCase를 사용합니다.
예: handleChange
, onSubmit
, handleClickSaveButton
Context
Context 객체는 PascalCase를 사용하며 Context 접미사를 붙입니다.
예: ThemeContext
, UserContext
Custom Hook
use 접두사를 사용하여 camelCase로 이름을 지정합니다.
예: useFetch
, useLocalStorage
상수
대문자와 밑줄을 사용합니다.
예: const API_ENDPOINT = "https://api.example.com";
기본 규칙
루트 페이지: pages 디렉토리 바로 아래에 위치한 파일은 웹사이트의 루트 경로에 매핑됩니다.
예: pages/index.js
-> /
하위 경로: pages 디렉토리의 하위 폴더는 URL의 하위 경로와 매핑됩니다.
예: pages/about.js
-> /about
예: pages/blog/post.js
-> /blog/post
동적 라우팅: 파일 또는 폴더 이름에 대괄호([]
)를 사용하여 동적 라우팅을 표현할 수 있습니다.
예: pages/blog/[slug].js
-> /blog/:slug
(여기서 :slug는 동적으로 변경될 수 있는 부분)
파일명
파일 이름은 kebab-case (소문자와 하이픈 조합)
예: user-profile.js
, userList.js
동적 라우팅: 동적 부분을 대괄호로 감싸서 표현합니다. 이때도 kebab-case를 주로 사용합니다.
예: [post-id].js
, [username].js
루트 파일: 루트 페이지를 나타내는 파일의 이름은 항상 index.js
(또는 .jsx
, .tsx
등의 확장자)입니다.
Next.js에서 API 라우트는 pages/api
디렉토리 아래에 위치합니다. 이 디렉토리 내의 파일 네이밍도 위와 동일한 규칙을 따릅니다.
페이지 파일명은 주로 URL의 경로와 연관이 있기 때문에, URL에서 자연스럽게 보이도록 kebab-case를 사용하는 것이 좋습니다.
먼저 컴포넌트는 재사용 가능한 독립적인 UI 단위를 말합니다. 이걸 Atomic Design으로도 설명하는데 최소단위로 쪼갠 UI를 조합하여 하나의 그룹을 만들고 그 그룹이 뭉쳐서 하나의 페이지를 구성하는 형식입니다.
컴포넌트 정의
import React from 'react';
export const Hello = (props) => <h1>Hello, {props.name}!</h1>;
컴포넌트 사용
import React from 'react';
import { Hello } from './Hello';
const App = () => {
return (
<div>
<Hello name="Hoge" />
</div>
);
}
props는properties
의 줄임말로, 컴포넌트 간에 데이터를 전달할 때 사용하는 객체입니다. 부모 컴포넌트에서 자식 컴포넌트로 값을 전달하는 방식입니다.
props에 다양한 데이타의 유형(숫자, 배열등..) 을 전달해 줄 수 있으며 특히 함수 및 리액터 컴포넌트도 전달해 줄수 있습니다.
<Component message="Hello, World!" />
<Component age={30} />
<Component isActive={true} />
<Component list={['apple', 'banana', 'cherry']} />
<Component user={{name: 'John', age: 30}} />
<Component onClick={() => console.log('Button clicked!')} />
<Component header={<Header />} footer={<Footer />} />
// 사용예
const Component = (props) => {
return (
<div>
<h1>{props.message}</h1>
<p>Age: {props.age}</p>
<ul>
{props.list.map(item => <li key={item}>{item}</li>)}
</ul>
<button onClick={props.onClick}>Click me!</button>
<div className="header">{props.header}</div>
<div className="footer">{props.footer}</div>
</div>
);
};
이를 이용함으로써 로컬스코프 CSS를 제공하여 다른 CSS와의 충돌없이 독립적으로 스타일을 지정 할 수 있습니다. (컴포넌트 단위로 만들어 지정하면 될듯합니다.)
로컬스코프 CSS작성
/* button.module.css */
.button {
background-color: blue;
color: white;
}
사용예
import React from 'react';
import classes from './button.module.css';
const Button = () => {
return <button className={classes.button}>Click me</button>;
};
export default Button;
Link 컴포넌트는 사용자가 페이지를 전환할 때 전체 페이지 리로드 없이 클라이언트 사이드에서 뷰를 전환하는데 사용됩니다.
import Link from 'next/link';
function Navigation() {
return (
<nav>
<Link href="/">Home</Link>
<Link href="/about">About</Link>
<Link href="/contact">Contact</Link>
</nav>
);
}
컴포넌트를 루프를 사용하여 생성하는 것은 React에서 자주 사용되는 패턴입니다. 주로 배열 데이터를 기반으로 여러 개의 컴포넌트를 동적으로 렌더링할 때 사용됩니다.
const data = [
{ id: 1, name: 'Minji' },
{ id: 2, name: 'DongHyuk' },
{ id: 3, name: 'Tom' }
];
// 컴포넌트 정의
const Item = ({ name }) => <li>{name}</li>;
// 루프를 이용하여 반복생성
const ItemList = () => (
<ul>
{data.map(item => (
<Item key={item.id} name={item.name} />
))}
</ul>
);
중요!
반복되는 컴포넌트에는 반드시 중복되지 않는 값을 Key로 지정해야한다.
// 이벤트 부분(handleClick)를 따로 빼주는게 좋음
const MyComponent = () => {
const handleClick = () => {
alert('Button clicked!');
};
// 좋은 사용예
return (
<button onClick={handleClick}>
Click me
</button>
);
}
// 나쁜 사용예
<button onClick={() => alert('Button clicked!')}>
Click me
</button>
useCallback를 이용하는것이 성능상 좋으나 나중에 따로 설명하겠습니다.
useEffect의 동작 방식은 두 번째 매개변수인 dependency array
의 값에 따라 다릅니다.
Dependency array가 없을 경우는 컴포넌트가 매번 렌더링 될 때마다 useEffect 내부의 함수가 실행됩니다.
Dependency array가 비어 있을 경우 ([])엔 컴포넌트가 마운트 될 때 한 번만 실행되며 언마운트 될 때 cleanup 함수 (선택적으로 정의할 수 있음)가 실행됩니다.
Dependency array에 값이 있을땐 해당 값들 중 어떤 것이라도 변경되면 useEffect 내부의 함수가 실행됩니다.
import { useEffect, useState } from 'react';
const MyComponent = () => {
const [data, setData] = useState(null);
useEffect(() => {
const fetchData = async () => {
const response = await fetch('https://api.example.com/data');
const result = await response.json();
setData(result);
};
fetchData();
}, []); // Dependency array가 비어 있으므로 컴포넌트가 마운트될 때만 실행됩니다.
return (
<div>
{data ? data.map(item => <div key={item.id}>{item.name}</div>) : 'Loading...'}
</div>
);
}
useState는 컴포넌트의 로컬 상태를 관리하기 위해 사용됩니다. 함수형 컴포넌트에서 상태 관리의 기능을 추가할 수 있게 해줍니다.
useState는 초기값을 매개변수로 받아 상태값과 해당 상태를 업데이트하는 함수를 배열로 반환합니다.
state: 현재상태값
setState: 상태를 업데이트하는 함수
initialValue: 초기상태값
const [state, setState] = useState(0);
setState는 useState 훅에서 반환된 상태 업데이트 함수입니다. 이 함수를 사용하여 상태를 변경하면 컴포넌트가 리렌더링됩니다.
// 직접 값을 전달
setCount(5); // count를 5로 설정
// 함수를 전달
setCount(prevCount => prevCount + 1); // 이전 count 값에 1을 더함
사용예
import React, { useState } from 'react';
const Counter = () => {
const [count, setCount] = useState(0);
const handleIncrement = () => {
setCount(prevCount => prevCount + 1);
};
return (
<div>
<p>You clicked {count} times</p>
<button onClick={handleIncrement}>Click me</button>
</div>
);
};
export default Counter;
기본 React Hooks (useState, useEffect, useContext 등)의 로직을 재사용하기 위해 사용자가 직접 정의한 Hooks입니다.
기본적으로 Custom Hook
은 함수로 정의되며, 해당 함수 내부에서 여러 기본 Hooks를 사용하여 원하는 로직을 구현할 수 있습니다.
Custom Hook
의 이름은 use~
로 시작해야 합니다.
(중요! 명명규칙으로 Hooks이라는 것을 알수 있게 하기 위함입니다.)
여러 컴포넌트에서 동일한 로직을 사용해야 할 때 Custom Hook을 생성하여 해당 로직을 재사용할 수 있습니다.
또한 컴포넌트 내부에 복잡한 로직이 포함된 경우, 해당 로직을 Custom Hook으로 분리하여 컴포넌트의 가독성을 높일 수 있습니다.
Hook의 정의예시
import { useState } from 'react';
const useCounter = (initialValue = 0) => {
const [count, setCount] = useState(initialValue);
const increment = () => setCount(prevCount => prevCount + 1);
const decrement = () => setCount(prevCount => prevCount - 1);
return [count, increment, decrement];
};
export default useCounter;
Hook의 사용예시
import React from 'react';
import useCounter from './path_to_useCounter';
const CounterComponent = () => {
const [count, increment, decrement] = useCounter(0);
return (
<div>
<button onClick={decrement}>-</button>
<span>{count}</span>
<button onClick={increment}>+</button>
</div>
);
};
export default CounterComponent;
React hook 중 하나로 불필요한 연산을 방지하고 연산된 값을 재사용하는 데 도움을 줍니다.
즉, 주어진 의존성 배열 내의 값들이 변경되지 않았다면 이전에 계산된 값을 반환하게 됩니다.
const memoizedValue = useMemo(() => computeExpensiveValue((a, b), [a, b]);
이 경우 a나 b의 값이 변경되지 않았다면 computeExpensiveValue 함수는 다시 호출되지 않고 이전에 반환된 값을 사용합니다.
import { useMemo } from 'react';
const Component = ({ list, filter }) => {
const filteredList = useMemo(() => {
console.log("Filtering list");
return list.filter(item => item.includes(filter));
}, [list, filter]);
return <div>{filteredList.join(', ')}</div>;
};
Next.js의 hook 중 하나로, 현재 라우팅에 관한 정보와 라우팅 메소드를 제공합니다.
const router = useRouter();
import { useRouter } from 'next/router';
const NavigationButton = () => {
const router = useRouter();
const navigateToHome = () => {
router.push('/');
};
return <button onClick={navigateToHome}>Go to Home</button>;
};
useRouter는 페이지나 컴포넌트 내에서 현재의 라우트 정보에 접근하거나 라우트를 변경하고 싶을 때 유용하게 사용됩니다.
State 리프트업(Lifting State Up)은 React에서 상태 관리의 중요한 패턴 중 하나입니다.
이 패턴은 여러 컴포넌트가 동일한 변경 가능한 데이터를 공유해야 할 때 사용됩니다.
이러한 경우 상태를 가장 가까운 공통 조상 컴포넌트로 "리프트"하여 해당 상태를 공유하게 됩니다.
State 리프트업의 절차는 다음과 같습니다.
1. 상태를 공유해야 하는 컴포넌트를 확인한다.
2. 가장 가까운 공통 조상 컴포넌트를 찾는다.
3. 상태와 상태를 변경하는 함수를 해당 조상 컴포넌트로 이동한다.
4. 상태와 상태 변경 함수를 자식 컴포넌트에 props로 전달한다.
// 개별 입력 컴포넌트
const InputField = ({ value, onChange }) => {
return <input type="text" value={value} onChange={onChange} />;
};
// 상태를 공유하는 부모 컴포넌트
const CompareInputs = () => {
const [input1, setInput1] = useState("");
const [input2, setInput2] = useState("");
const areInputsEqual = input1 === input2;
return (
<div>
<InputField value={input1} onChange={(e) => setInput1(e.target.value)} />
<InputField value={input2} onChange={(e) => setInput2(e.target.value)} />
<p>{areInputsEqual ? "Inputs are equal" : "Inputs are not equal"}</p>
</div>
);
};
위 예제에서 InputField
컴포넌트는 입력 값을 관리하지 않습니다. 대신 부모 컴포넌트인 CompareInputs에서 상태를 관리하고, 상태 변경 함수와 현재 값은 props로 전달됩니다. 이렇게 하면 CompareInputs 컴포넌트에서 두 입력 필드의 값이 동일한지 쉽게 비교할 수 있습니다.
이러한 패턴을 사용하면 컴포넌트 간의 상호 작용을 명확하게 파악할 수 있으며, 상태 로직을 중앙에서 관리할 수 있어 유지 보수가 용이해집니다.
상태를 공유하기 위한 Context생성한다.
import React, { createContext, useState, useContext } from 'react';
// 상태를 공유하기 위한 Context생성
export const SharedDataContext = createContext();
export const useSharedData = () => {
return useContext(SharedDataContext);
};
export const SharedDataProvider = ({ children }) => {
const [sharedData, setSharedData] = useState("Initial data");
return (
<SharedDataContext.Provider value={{ sharedData, setSharedData }}>
{children}
</SharedDataContext.Provider>
);
};
_app.js
에서 SharedDataProvider
를 사용
import { SharedDataProvider } from './path-to-your-context-file';
function MyApp({ Component, pageProps }) {
return (
<SharedDataProvider>
<Component {...pageProps} />
</SharedDataProvider>
);
}
export default MyApp;
import React from 'react';
import SomeComponent from '../SomeComponent';
const Home = () => {
return (
<div>
<h1>Home Page</h1>
<SomeComponent />
</div>
);
};
export default Home;
다른 컴포넌트에서 상태사용하기
import { useSharedData } from './path-to-your-context-file';
function SomeComponent() {
const { sharedData, setSharedData } = useSharedData();
return (
<div>
<p>Shared Data: {sharedData}</p>
<button onClick={() => setSharedData("Updated data")}>
Update Shared Data
</button>
</div>
);
}
export default SomeComponent;
_app.js
에서 SharedDataProvider
컴포넌트를 사용하여 모든 페이지와 컴포넌트에 상태를 제공하면, 다른 컴포넌트에서 useSharedData
hook
을 이용하여 해당 상태에 접근하고 변경할 수 있습니다.
_app.js
(또는 _app.tsx
에 따라 다름)는 Next.js 프로젝트에서 전체 애플리케이션을 위한 커스텀 App 컴포넌트를 정의하는 곳입니다. 여기서 전체 애플리케이션에 걸쳐 사용되는 layout, CSS, 상태 관리 설정 등을 정의할 수 있습니다.
import '../styles/global.css'; // 전역 CSS
function MyApp({ Component, pageProps }) {
return (
// 여기에 추가하면 앱 전체에 사용됨.
<div className="appContainer">
<Component {...pageProps} />
</div>
);
}
export default MyApp;
이뮤터블(Immutable)이란 객체나 배열 등의 데이터 구조가 한 번 생성되면 그 후로 변경될 수 없는 특성을 말합니다.
즉, 이뮤터블 객체의 상태를 변경하려면 원본 객체를 수정하는 것이 아니라 새로운 객체를 생성해야 합니다.
이뮤터블 패턴은 데이터의 신뢰성과 예측 가능성을 높이는 데 중요한 역할을 합니다.
반대로 파괴적 메소드(Destructive)는 객체나 배열의 원래 상태를 변경하는 메소드를 말합니다.
예를 들어 JavaScript 배열의 push(), pop(), splice()와 같은 메소드들은 배열을 직접 수정하며, 이는 예기치 않은 부작용을 초래할 수 있습니다.
스프레드 구문 (...)은 배열이나 객체의 내용을 다른 배열이나 객체로 복사합니다.
원본 배열이나 객체를 직접 수정하는 것이 아니라 새로운 배열이나 객체를 생성할 수 있습니다.(중요!)
따라서 원본 데이터를 보호하면서 새로운 데이터 구조를 쉽게 만들 수 있습니다.
// 배열
const arr1 = [1, 2, 3];
const arr2 = [...arr1, 4, 5]; // [1, 2, 3, 4, 5]
// 객체
const obj1 = { a: 1, b: 2 };
const obj2 = { ...obj1, c: 3 }; // { a: 1, b: 2, c: 3 }
스프레드 구문은 특히 React에서 상태를 이뮤터블하게 관리할 필요가 있을 때 자주 사용됩니다.
사용예
import React, { useState } from 'react';
const UserListApp = () => {
const [users, setUsers] = useState([
{ id: 1, name: 'Donghyuk' },
{ id: 2, name: 'Minji' }
]);
const addUser = (name) => {
const newUser = { id: Date.now(), name };
setUsers([...users, newUser]);
};
const removeUser = (id) => {
const updatedUsers = users.filter(user => user.id !== id);
setUsers(updatedUsers);
};
return (
<div>
<ul>
{users.map(user => (
<li key={user.id}>
{user.name} <button onClick={() => removeUser(user.id)}>Remove</button>
</li>
))}
</ul>
<button onClick={() => addUser('New User')}>Add User</button>
</div>
);
};
export default UserListApp;
시간이 되면 VSCode를 이용해서 코딩할 때 유용한 팁을 정리할 생각입니다.
그럼 모두들 굿럭!
제 선배님의 블로그 글을 읽고 나서, 저는 정말 많은 것을 배웠습니다. 선배님의 글은 React와 Next.js에 대한 이해를 돕기 위해 필요한 모든 주요 개념들을 아주 잘 설명하고 있습니다.
글에서 가장 인상 깊었던 부분은 React와 Next.js가 어떻게 서로 상호 작용하는지에 대한 설명이었습니다. 선배님이 이 두 기술을 '절친'이라고 표현하신 것이 특히 기억에 남았습니다. 덕분에 이 두 기술이 어떻게 함께 작동하여 웹 애플리케이션을 구축하는데 도움이 되는지 명확하게 이해할 수 있었습니다.
선배님이 파일 확장자 선택에 관한 조언도 아주 유용했습니다. .tsx 확장자가 TypeScript의 타입 체크 기능과 JSX의 컴포넌트 정의 기능을 모두 사용할 수 있기 때문에 권장되는 선택임을 알려주셨습니다.
디렉토리 구성, 네이밍 컨벤션, 페이지와 컴포넌트 작성 방법 등 실제 프로젝트에서 바로 적용할 수 있는 실질적인 팁들도 많았습니다. 그리고 'use'로 시작하는 Hooks들과 Context API를 사용하여 상태 관리를 하는 방법 등 고급 주제들도 쉽게 이해할 수 있도록 잘 설명해주셨습니다.
마지막으로, 선배님이 코드 예시를 제공해준 것은 정말 큰 도움이 되었습니다. 코드 예시를 통해서 각 개념과 팁들이 실제로 어떻게 적용되는지 보여주셔서 추상적인 개념들을 좀 더 구체적으로 이해할 수 있었습니다.
전체적으로 볼 때, 선배님이 작성하신 글은 React와 Next.js 초보자부터 경험이 많은 개발자까지 모두가 유익하게 읽을 수 있는 내용입니다. 마치 전문가가 직접 가르쳐주는 것 같은 느낌을 받았습니다.
마지막으로, 선배님이 VSCode 팁에 대해서도 글을 작성해주실 계획이라고 하셨는데, 저는 그 글을 기대하며 기다리고 있겠습니다. 이번 글처럼 다음 글도 많은 도움이 될 것 같습니다.
정말 감사합니다, 선배님!