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
훅을 사용하여 프로그래매틱 네비게이션을 구현,
조건부 렌더링을 통해 인증된 사용자만 접근 가능한 라우트 생성.
참고사이트