React는 사용자 인터페이스를 구축하기 위한 JavaScript 라이브러리로, 몇 가지 핵심 철학을 바탕으로 설계되었다.
선언적 프로그래밍이란?
애플리케이션의 상태를 어떻게 변경할지를 명령형으로 지시하는 대신,
원하는 결과(UI)를 선언하는 방식을 의미
React의 핵심 철학중 하나다
React는 선언적 접근 방식을 채택한다.
개발자는 UI가 어떻게 보여야 하는지를 설명하고,
React는 이를 구현하는 세부사항을 처리한다.
ex)
const welcome = (props) => {
return <h1>Hello, {props.name}</h1>
}
이 코드는 "name" prop을 받아 메시지를 표시하는 컴포넌트를 선언적으로 정의한다.
React 애플리케이션은 재사용 가능한 컴포넌트로 구성된다.
이 접근 방식은 코드의 재사용성과 유지보수성을 향상시킨다.
ex)
const App = () => {
return (
<div>
<Header />
<Main />
<Footer />
</div>
)
}
App 컴포넌트는 Header,Main,Footer 컴포넌트로 구성되어 있다.
React에서 데이터는 항상 부모 컴포넌트에서 자식 컴포넌트로 간다.
이를 "단방향 데이터 흐름"이라고 한다.
ex)
const Parent = () => {
const [count, setCount] = useState(0)
return <Child count={count} onIncrement={() => setCount(count + 1)}/>
}
const Child = ({count, onIncrement}) => {
return (
<div>
<p>Count: {count}</p>
<button onClick={onIncrement}>Increment</button>
</div>
)
}
count 상태와 이를 변경하는 함수는 Parent 컴포넌트에 있으며,
props를 통해 child컴포넌트로 전달된다.
React 컴포넌트의 생명주기를 이해하는 건 효율적인 React 애플리케이션 개발의 핵심이다.함수형 컴포넌트에서는 Hooks를 사용하여 생명주기와 유사한 기능을 구현한다.
가장 많이 사용되는 Hook은 useState와 useEffect이다.
ex)
const ExampleComponent = () => {
const [count, setCount] = useState(0)
useEffect(() => {
console.log('Component mounted or update')
return () => {
console.log('Component will unmount or effect cleanup')
}
},[count])
return (
<div>
<p>{count}</p>
<button onClick={() => setCount(count + 1)}>Click</button>
</div>
)
}
useState와 useEffect를 사용하여 상태 관리와 생명주기 관련 로직을 구현.
useEffect Hook을 사용하여 클래스 컴포넌트의 생명주기 메서드와 유사한 동작을 구현할 수 있다.
useEffect(() => {
console.log('Component mounted')
},[]) // 빈 의존성 배열
useEffect(() => {
console.log('Component updated')
}) // 의존성 배열 없음
useEffect(() => {
return () => {
console.log('Component will unmount')
}
},[]) // 빈 의존성 배열
이런 방식으로 useEffect를 사용하면 클래스 컴포넌트의 생명주기 메서드와 유사한 기능을 구현할 수 있다.
상태(state)란?
컴포넌트 내에서 관리되는 데이터를 말한다.
특징
1.가변성: 상태는 시간이 지남에 따라 변할 수 있다.
2.컴포넌트 특징: 각 컴포넌트는 자신만의 상태를 가질 수 있다.
3.렌더링 영향: 상태가 변경되면 해당 컴포넌트는 re-rendering된다.
4.비동기적 업데이트: React는 성능 최적화를 위해 상태 업데이트를 비동기적으로 처리 가능하다.
5.단방향 데이터 흐름: 부모 컴포넌트에서 자식 컴포넌트로 props를 통해 전달된다.
효과적인 상태 관리는 React 애플리케이션 개발의 핵심이다.
useState는 간단한 상태 관리에 적합, useReducer는 더 복잡한 상태 로직을 다룰 때 유용하다.
const Counter = () => {
const [count, setCount] = useState(0)
return (
<div>
<p>{count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
)
}
주요 특징
1.상태 로직 분리: 상태 업데이트 로직을 컴포넌트에서 분리 가능
2.예측 가능한 상태 변화:action객체를 통해 상태 변화를 명시적으로 정의
3.복잡한 상태 관리: 여러 하위 값을 가진 복잡한 상태를 효과적으로 관리 가능
4.성능 최적화: 상태 업데이트 로직이 복잡한 경우useReducer가 더 나은 성능을 제공
useReducer는 (reducer, initialState) 두 가지 주요 인자를 받는다.
reducer: 현재 상태와 액션을 받아 새로운 상태를 반환하는 함수initialState: 초기 상태useReducer는 [state, dispatch] 배열을 반환한다.
state: 현재 상태dispatch: 액션을 발생시키는 함수ex)
const reducer = (state,action) => {
switch (action.type){
case 'increment':
return {count: state.count + 1}
case 'decrement':
return {count: state.count - 1}
default:
throw new Error()
}
}
const Counter = () => {
const [state, dispatch] = useReducer(reducer, initialState)
return (
<div>
{state.count}
<button onClick={() => dispatch({type: 'increment'})}>+</button>
<button onClick={() => dispatch({type: 'decrement'})}>-</button>
</div>
)
}
useReducer는 복잡한 상태 로직을 다룰 때 더 구조화된 접근 방식을 제공한다.
Context API란?
React에서 제공하는 상태 관리 도구이다.
컴포넌트 트리 전체에 데이터를 전달할 수 있게 해주는 기능이다.
Context API는 prop drilling없이 컴포넌트 트리 전체에 데이터를 제공 가능하다.
ex)
const ThemeContext = React.createContext('light')
const App = () => {
return (
<ThemeContext.Provider value='dark'>
<Toolbar />
</ThemeContext.Provider>
)
}
const Toolbar = () => {
return (
<div>
<ThemedButton />
</div>
)
}
const ThemedButton = () => {
const theme = useContext(ThemeContext)
return <button style={{background: theme === 'dark' ? 'black':'white'}}>context</button>
}
Context API를 사용하면 중간 컴포넌트를 통해 props를 전달하지 않고도 컴포넌트 트리의 깊은 곳 까지 데이터를 전달 가능하다.
Redux: 대규모 애플리케이션, 복잡한 상태 로직, 시간 여행 디버깅MobX: 더 적은 보일러플레이트로 반응형 프로그래밍을 선호 시Recoil: React에 특화된 상태 관리, 비동기 상태 관리가 필요할 때선택 기준:
React.memo:
컴포넌트의 불필요한 리렌더링을 방지
const testComponent = React.memo(({prop1, prop2}) => {
// 렌더링 로직
})
useMemo:
계산 비용이 높은 값을 메모이제이션
const memoizedValue = useMemo(() => computeExpensiveValue(a,b),[a,b])
useMemo는 의존성 배열([a,b])의 값이 변경될 때만 computeExpensiveValue함수를 재실행한다.
그외의 경우, 이전에 계산된 값을 재사용하여 불필요한 계산을 방지한다.
특히 복잡한 계산이나 큰 데이터 처리에 유용하다
useCallback:
콜백 함수를 메모이제이션
const memoizedCallback = useCallback(
() => {
doSomething(a,b)
},
[a, b]
)
useCallback은 의존성 배열([a,b])의 값이 변경될 때만 새로운 함수를 생성한다.
불필요한 함수 재생성을 방지하고, 특히 자식 컴포넌트에 콜백을 props로 전달할 때 유용하다.
자식 컴포넌트가 React.memo로 최적화되어 있다면, 불필요한 리랜더링을 방지할 수 있다.
이런 최적화 기법들은 불필요한 재계산과 리렌더링을 방지하여 애플리케이션의 성능을 향상시킨다.
React는 가상DOM을 사용하여 실제 DOM 업데이트를 최적화한다.
재조정 과정에서 React는 이전 가상 DOM과 새로운 가상DOM을 비교하여 최소한의 변경사항만 실제DOM에 적용한다.
React DevTools의 Profiler를 사용하여 성능을 분석하고 병목 현상을 식별 가능하다.
렌더링 시간이 긴 컴포넌트를 찾아 최적화할 수 있다.
HOC는 컴포넌트 로직을 재사용하기 위한 고급 기법이다.
const withSubscription = (WrappedComponent, selectData) => {
return class extends React.Component {
constructor(props){
super(props)
this.state = {
data: selectData(DataSource, props)
}
}
// ...생략
render() {
return <WrappedComponent data={this.state.data} {...this.props}/>
}
}
}
withSubscription은 WrappedComponent와selectData함수를 인자로 받는다.
DataSource로부터 데이터를 가져와 상태로 관리하는 기능을 추가한다.
WrappedComponent data={this.state.data} {...this.props} />에서 data prop과 함께 기존 모든 props를 전달한다.
HOC를 사용하면 여러 컴포넌트 간에 공통 기능을 쉽게 공유할 수 있다.
과도한 사용은 컴포넌트 계층을 복잡하게 만들수 있다.
React Hooks의 등장으로 일부 사용 사례가 대체되었지만, 여전히 유용한 패턴이다.
Render Props는 컴포넌트 간에 값을 공유하는 유연한 방법이다.
const Mouse = ({render}) => {
const [state, setState] = useState({x: 0, y: 0})
const handleMouseMove = (e) => {
setState({
x: e.clientX,
y: e.clientY
})
}
return (
<div onMouseMove={handleMouseMove}>
{render(state)}
</div>
)
}
// 사용
<Mouse render={mouse =>(
<p>{mouse.x}, {mouse.y}</p>
)}/>
Render Props 패턴을 사용하면 컴포넌트의 동작을 유연하게 확장할 수 있다.
Compound Components 패턴은 관련된 컴포넌트를 그룹화하고 상태를 공유하는데 유용하다.
const Menu = ({children}) => {
const [activeIndex, setActiveIndex] = useState(0)
return React.Children.map(children, (child, index) =>
React.cloneElement(child, {activeIndex, setActiveIndex, index})
)
}
const MenuItem = ({children, activeIndex, setActiveIndex, index}) => {
return (
<div onClick={() => setActiveIndex(index)}>
{activeIndex === index ? '> ' : ''}{children}
</div>
)
}
Menu.Item = MenuItem
// 사용
<Menu>
<Menu.Item>Home</Menu.Item>
<Menu.Item>About</Menu.Item>
<Menu.Item>Contact</Menu.Item>
</Menu>
Compound Components 패턴은 관련된 컴포넌트들을 논리적으로 그룹화하고 내부 상태를 공유하는데 효과적이다.
interface Props {
name: string
age: number
inStudent?: boolean
}
interface State {
count: number
}
const testComponent: React.FC<Props> = ({name, age, inStudent = false}) => {
const [count, setCount] = useState<number>(0)
}
타입스크립트 사용 시 컴포넌트의 props와 state에 대한 타입 안정성 확보가 가능하다
interface ListProps<T> {
items: T[]
renderItem: (item: T) => React.ReactNode
}
const List = <T,>({items, renderItem}: ListProps<T>) => {
return (
<ul>
{items.map((item, index) => (
<li key={index}>{renderItem(item)}</li>
))}
</ul>
)
}
const useCount = (initialValue:number): [number, () => void] => {
const [count, setCount] = useState(initialValue)
const increment = useCallback(() => setCount(prev => prev + 1) ,[])
}
const Counter: React.FC = () => {
const [count, increment] = useState(0)
return (
<div>
<p>{count}</p>
<button onClick={increment}>Increment</button>
</div>
)
}
타입스크립트와 Hooks를 결합하면 커스텀 훅의 입력과 출력에 대한 타입 안정성 확보가 가능하다
import React, { useState, useEffect } from 'react';
const DataFetcher = () => {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchData = async () => {
try {
const response = await fetch('https://api.example.com/data');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
setData(result);
} catch (e) {
setError(e.message);
} finally {
setLoading(false);
}
};
fetchData();
}, []);
if (loading) return 'Loading...';
if (error) return `Error: ${error}`;
return <div>{JSON.stringify(data)}</div>;
};
export default DataFetcher;
useEffect를 사용하여 컴포넌트 마운트 시 데이터를 가져오고 로딩 상태와 에러 처리를 구현할 수 있다.
React query와SWR은 둘 다React애플리케이션에서 데이터fetching을 간소화하고 최적화 하는 라이브러리다.
캐싱, 자동 재검증, 에러 처리 등의 기능을 제공한다.
useQuery훅을 사용하며, 쿼리 키와 fetcher 함수를 인자로 받는다.isLoading,error,data등을 반환한다.import { useQuery } from 'react-query'
const fetchRepoData = async () => {
try {
const response = await fetch('https://api.github.com/repos/tannerlinsley/react-query');
if (!response.ok) {
throw new Error('Network response was not ok');
}
return await response.json();
} catch (error) {
throw error;
}
};
const Example = () => {
const { isLoading, error, data } = useQuery('repoData', fetchRepoData);
if (isLoading) return 'Loading...';
if (error) return 'An error has occurred: ' + error.message;
return (
<div>
<h1>{data.name}</h1>
<p>{data.description}</p>
<strong>{data.subscribers_count}</strong>{' '}
<strong>{data.stargazers_count}</strong>{' '}
<strong>{data.forks_count}</strong>
</div>
);
};
useSWR훅을 사용하며, 키(보통 URL)와 fetcher 함수를 인자로 받는다.data,error를 반환하며, 로딩 상태는 !data && !error로 확인한다.import useSWR from 'swr'
const Profile = () => {
const { data, error } = useSWR('/api/user', fetcher)
if (error) return <div>failed to load</div>
if (!data) return <div>loading...</div>
return <div>hello {data.name}!</div>
}
React에서 비동기 데이터 로딩을 더 쉽고 선언적으로 처리할 수 있게 해주는 기능이다.
특징
선언적 로딩 상태: 컴포넌트가 데이터를 기다리는 동안 표시할 로딩 UI를 쉽게 지정 가능하다.
간소화된 코드: 복잡한 로딩 상태 관리 로직을 줄이고, 더 깔끔한 컴포넌트 코드를 작성할 수 있다.
계단식 로딩: 중접된 Suspense 컴포넌트를 사용해 세분화된 로딩 경험을 제공할 수 있다.
데이터 요청 조정: React가 여러 데이터 요청을 자동으로 조정하여 성능을 최적화한다.
에러 처리 통합: Error Boundary와 함께 사용하여 데이터 로딩 오류를 효과적으로 처리 가능하다.
const resource = fetchProfileData()
const ProfilePage = () => {
return (
<Suspense fallback={<h1>Loading profile...</h1>}>
<ProfileDetails />
<Suspense fallback={<h1>Loading posts...</h1>}>
<ProfileTimeline />
</Suspense>
</Suspense>
)
}
const ProfileDetails = () => {
const user = resource.user.read()
return <h1>{user.name}</h1>
}
const ProfileTimeline = () => {
const posts = resource.posts.read()
return (
<ul>
{posts.map(post => (
<li key={post.id}>{post.text}</li>
))}
</ul>
)
}
Suspense를 사용하면 데이터 로딩 상태를 선언적으로 처리할 수 있다.
import { BrowserRouter as Router, Route, Link, Routes } from 'react-router-dom';
const App = () => {
return (
<Router>
<div>
<nav>
<ul>
<li><Link to="/">Home</Link></li>
<li><Link to="/about">About</Link></li>
<li><Link to="/users">Users</Link></li>
</ul>
</nav>
<Routes>
<Route path="/about" element={<About />} />
<Route path="/users" element={<Users />} />
<Route path="/" element={<Home />} />
</Routes>
</div>
</Router>
);
}
// route.tsx
const router = createBrowserRouter([
{
path: "/",
element: <RootLayout />,
children: [
{
path: "/",
element: <Home />,
},
{
path: "/project",
element: <Project />,
},
{
path: "/skill",
element: <Skill />,
},
{
path: "/about",
element: <About />,
},
],
},
])
export default router
// app.tsx
const App = () => {
return (
<>
<RouterProvider router={router} />
</>
)
}
실제 사용(로그인이 있을 경우)
const ADMIN_ROUTER = {
element: <AdminRoute />,
children: [
{
path: "",
element: <ProductListPage />,
},
{
path: "/detail-product/:productId",
element: <DetailProductPage />,
},
{
path: "/register-product",
element: <RegisterProductPage />,
},
{
path: "/used-product",
element: <UsedProductPage />,
},
{
path: "/my-info",
element: <MyInfoPage />,
},
{
path: "/*",
element: <NotFoundPage />,
},
],
};
const PUBLIC_ROUTER = [
{
path: "/signin",
element: <SigninPage />,
},
{
path: "/signup",
element: <SignupPage />,
},
{
path: "/*",
element: <NotFoundPage />,
},
];
const router = createBrowserRouter([
{
children: [...PUBLIC_ROUTER, ADMIN_ROUTER],
},
]);
export default router;
React Router v6를 사용하면 선언적 방식으로 라우팅을 구현할 수 있다.
import { lazy, Suspense } from 'react';
import { BrowserRouter as Router, Route, Routes } from 'react-router-dom';
const Home = lazy(() => import('./routes/Home'));
const About = lazy(() => import('./routes/About'));
const App = () => {
return (
<Router>
<Suspense fallback={<div>Loading...</div>}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
</Routes>
</Suspense>
</Router>
);
}
lazy와 Suspense를 사용하여 코드 스플리팅과 지연 로딩을 구현.
import { useNavigate } from 'react-router-dom';
const LoginButton = () => {
let navigate = useNavigate();
const handleClick = () => {
navigate('/home');
}
return <button onClick={handleClick}>Login</button>;
}
// 인증 처리
const PrivateRoute = ({ children }) => {
const auth = useAuth();
return auth ? children : <Navigate to="/login" />;
}
<Route
path="/protected"
element={
<PrivateRoute>
<ProtectedPage />
</PrivateRoute>
}
/>
useNavigate훅을 사용하여 프로그래매틱 네비게이션을 구현,
조건부 렌더링을 통해 인증된 사용자만 접근 가능한 라우트 생성.
참고사이트