React 완벽 동작 가이드

ijimlnosk·2024년 8월 5일
3

React

목록 보기
2/7
post-thumbnail

React의 기본 철학 이해하기

React는 사용자 인터페이스를 구축하기 위한 JavaScript 라이브러리로, 몇 가지 핵심 철학을 바탕으로 설계되었다.

1.1 선언적 프로그래밍

선언적 프로그래밍이란?
애플리케이션의 상태를 어떻게 변경할지를 명령형으로 지시하는 대신,
원하는 결과(UI)를 선언하는 방식을 의미
React의 핵심 철학중 하나다

React는 선언적 접근 방식을 채택한다.
개발자는 UI가 어떻게 보여야 하는지를 설명하고,
React는 이를 구현하는 세부사항을 처리한다.

ex)

const welcome = (props) => {
	return <h1>Hello, {props.name}</h1>
}

이 코드는 "name" prop을 받아 메시지를 표시하는 컴포넌트를 선언적으로 정의한다.

1.2 컴포넌트 기반 아키텍처

React 애플리케이션은 재사용 가능한 컴포넌트로 구성된다.
이 접근 방식은 코드의 재사용성과 유지보수성을 향상시킨다.

ex)

const App = () => {
	return (
    	<div>
        	<Header />
        	<Main />
        	<Footer />
        </div>
    )
}

App 컴포넌트는 Header,Main,Footer 컴포넌트로 구성되어 있다.

1.3 단방향 데이터 흐름

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컴포넌트로 전달된다.

2.React 컴포넌트 생명주기 마스터하기

  • React 컴포넌트의 생명주기를 이해하는 건 효율적인 React 애플리케이션 개발의 핵심이다.

2.1 함수형 컴포넌트와 Hooks

함수형 컴포넌트에서는 Hooks를 사용하여 생명주기와 유사한 기능을 구현한다.
가장 많이 사용되는 HookuseStateuseEffect이다.

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>
  )
}

useStateuseEffect를 사용하여 상태 관리와 생명주기 관련 로직을 구현.

2.2 useEffect를 이용한 생명주기 시뮬레이션

useEffect Hook을 사용하여 클래스 컴포넌트의 생명주기 메서드와 유사한 동작을 구현할 수 있다.

  • componentDidMount:
    컴포넌트가 처음 렌더링된 직후에 호출되는 메서드
    • 외부 데이터 로딩 (API 호출 등)
    • DOM 조작
    • 이벤트 리스너 등록
useEffect(() => {
	console.log('Component mounted')
},[]) // 빈 의존성 배열
  • componentDidUpdate:
    컴포넌트가 업데이트될 때마다 호출되는 메서드
    • props나 state의 변경에 따른 부수 효과 처리
    • 조건부 네트워크 요청
useEffect(() => {
	console.log('Component updated')
}) // 의존성 배열 없음
  • componentWillUnmount:
    컴포넌트가 제거되기 직전에 호출되는 메서드
    • 이벤트 리스너 제거
    • 타이머 해제
    • 구독 취소
useEffect(() => {
	return () => {
    	console.log('Component will unmount')
    }
},[]) // 빈 의존성 배열

이런 방식으로 useEffect를 사용하면 클래스 컴포넌트의 생명주기 메서드와 유사한 기능을 구현할 수 있다.

3.상태 관리의 모든 것

상태(state)란?

컴포넌트 내에서 관리되는 데이터를 말한다.

특징

1.가변성: 상태는 시간이 지남에 따라 변할 수 있다.
2.컴포넌트 특징: 각 컴포넌트는 자신만의 상태를 가질 수 있다.
3.렌더링 영향: 상태가 변경되면 해당 컴포넌트는 re-rendering된다.
4.비동기적 업데이트: React는 성능 최적화를 위해 상태 업데이트를 비동기적으로 처리 가능하다.
5.단방향 데이터 흐름: 부모 컴포넌트에서 자식 컴포넌트로 props를 통해 전달된다.

효과적인 상태 관리는 React 애플리케이션 개발의 핵심이다.

3.1 useState와 useReducer 심층 분석

useState는 간단한 상태 관리에 적합, useReducer는 더 복잡한 상태 로직을 다룰 때 유용하다.

  • useState:
const Counter = () => {
	const [count, setCount] = useState(0)
    return (
    	<div>
	        <p>{count}</p>
	        <button onClick={() => setCount(count + 1)}>Increment</button>
        </div>
    )
}
  • useReducer:

    주요 특징
    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는 복잡한 상태 로직을 다룰 때 더 구조화된 접근 방식을 제공한다.

3.2 Context API 활용하기

Context API란?
React에서 제공하는 상태 관리 도구이다.
컴포넌트 트리 전체에 데이터를 전달할 수 있게 해주는 기능이다.

Context APIprop 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를 전달하지 않고도 컴포넌트 트리의 깊은 곳 까지 데이터를 전달 가능하다.

3.3 Redux vs MobX vs Recoil: 언제 무엇을 선택?

  • Redux: 대규모 애플리케이션, 복잡한 상태 로직, 시간 여행 디버깅
  • MobX: 더 적은 보일러플레이트로 반응형 프로그래밍을 선호 시
  • Recoil: React에 특화된 상태 관리, 비동기 상태 관리가 필요할 때

선택 기준:

  • 프로젝트 규모와 복잡도
  • 팀의 학습 곡선
  • 성능 요구사항
  • 상태 관리의 유연성 필요 정도
    각 라이브러리의 특징을 고려하여 프로젝트에 가장 적합한 솔루션을 선택하는 것이 중요하다.

4. React.memo, useMemo, useCallback 완벽 가이드

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로 최적화되어 있다면, 불필요한 리랜더링을 방지할 수 있다.

이런 최적화 기법들은 불필요한 재계산과 리렌더링을 방지하여 애플리케이션의 성능을 향상시킨다.

4.2 가상 DOM과 재조정(Reconcilliation) 이해하기

React는 가상DOM을 사용하여 실제 DOM 업데이트를 최적화한다.
재조정 과정에서 React는 이전 가상 DOM과 새로운 가상DOM을 비교하여 최소한의 변경사항만 실제DOM에 적용한다.

4.3 성능 프로파일링과 병목 현상 해결

React DevToolsProfiler를 사용하여 성능을 분석하고 병목 현상을 식별 가능하다.
렌더링 시간이 긴 컴포넌트를 찾아 최적화할 수 있다.

5.고급 패턴과 기법

고차 컴포넌트 (HOC) 마스터하기

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}/>
      }
    }
}

withSubscriptionWrappedComponentselectData함수를 인자로 받는다.
DataSource로부터 데이터를 가져와 상태로 관리하는 기능을 추가한다.
WrappedComponent data={this.state.data} {...this.props} />에서 data prop과 함께 기존 모든 props를 전달한다.

HOC를 사용하면 여러 컴포넌트 간에 공통 기능을 쉽게 공유할 수 있다.
과도한 사용은 컴포넌트 계층을 복잡하게 만들수 있다.
React Hooks의 등장으로 일부 사용 사례가 대체되었지만, 여전히 유용한 패턴이다.

5.2 Render Props 패턴 활용

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 패턴을 사용하면 컴포넌트의 동작을 유연하게 확장할 수 있다.

5.3 Compound Components 패턴 이해와 구현

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 패턴은 관련된 컴포넌트들을 논리적으로 그룹화하고 내부 상태를 공유하는데 효과적이다.

6.React와 타입 스크립트의 완벽 조화

6.1 타입스크립트로 props와 state 타입 정의

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에 대한 타입 안정성 확보가 가능하다

6.2 제네릭 컴포넌트 만들기

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>
    )        
}

타입스크립트와 Hooks의 결합

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를 결합하면 커스텀 훅의 입력과 출력에 대한 타입 안정성 확보가 가능하다

7. 비동기 처리와 데이터 페칭

7.1 useEffect를 이용한 데이터 페칭

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를 사용하여 컴포넌트 마운트 시 데이터를 가져오고 로딩 상태와 에러 처리를 구현할 수 있다.

7.2 React Query와 SWR 비교 분석

React querySWR은 둘 다 React 애플리케이션에서 데이터 fetching을 간소화하고 최적화 하는 라이브러리다.
캐싱, 자동 재검증, 에러 처리 등의 기능을 제공한다.

  • React Query:
    • 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>
  );
};
  • SWR:
    • 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>
}

7.3 Suspense for Data Fetching 이해하기

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를 사용하면 데이터 로딩 상태를 선언적으로 처리할 수 있다.

8. React 라우팅 마스터 클래스

8.1 React Router v6 완벽? 가이드

1.BrowserRouter를 사용

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>
  );
}
  • 간단하고 직관적인 구조
  • 컴포넌트 내에서 라우팅 로직을 직접 볼 수 있어 이해하기 쉽다.
  • 작은 규모의 앱에 적합

2. createBrowserRouter를 사용

// 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를 사용하면 선언적 방식으로 라우팅을 구현할 수 있다.

8.2 코드 스플리팅과 지연 로딩 구현

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>
  );
}

lazySuspense를 사용하여 코드 스플리팅과 지연 로딩을 구현.

8.3 프로그래매틱 네비게이션과 인증 처리

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


참고사이트

0개의 댓글