브라우저가 웹 페이지를 화면에 표시하기 위해 거치는 과정을 브라우저 렌더링 파이프라인이라고 합니다.
이 과정은 크게 6단계로 나눌 수 있습니다.

 

첫번째로 DOM 생성입니다.
브라우저가 HTML 파일을 받으면, 이 파일을 바이트(byte) 단위로 읽기 시작합니다. 브라우저의 HTML 파서(Parser)는 이 바이트들을 문자(character)로 변환하고, 이 문자들을 다시 HTML 토큰으로 변환합니다. 이 HTML 토큰들은 각각의 태그와 그 안에 포함된 텍스트, 속성 등을 의미하게 됩니다.

HTML 토큰이 생성되면, 브라우저는 이를 기반으로 DOM 트리를 생성합니다. DOM 트리는 HTML 문서의 구조를 트리 형태로 표현한 것으로, 각 태그가 노드(node)가 되어 부모-자식 관계를 형성합니다. 예를 들어, <body> 태그 아래에 <div>가 있다면, DOM 트리에서도 <body> 노드 아래에 <div> 노드가 있게 됩니다.


 

두번째로 CSSOM 생성입니다.
브라우저는 CSS 파일을 파싱(parse)합니다. CSS 파일 역시 바이트로 전송되므로, 브라우저는 이를 문자로 변환한 뒤, CSS 규칙으로 나눕니다. 각 CSS 규칙은 선택자(selector)와 선언(declaration)으로 구성되는데, 선택자는 스타일을 적용할 HTML 요소를 정의하고, 선언은 적용할 스타일을 정의합니다.

브라우저는 이 CSS 규칙들을 기반으로 CSSOM 트리를 생성합니다. CSSOM 트리는 DOM과 유사하게 트리 구조를 가지며, 각 노드는 해당 노드에 적용될 스타일 정보를 포함합니다.


 

세번째로 렌더 트리 생성입니다.
이제 브라우저는 DOM과 CSSOM을 결합하여 렌더 트리를 생성합니다. 렌더 트리는 화면에 실제로 표시될 요소들로만 구성됩니다. 예를 들어 display: none 속성이 있는 요소는 렌더 트리에 포함되지 않습니다. 이는 렌더 트리가 실제로 화면에 그려질 요소들만을 포함하기 때문입니다.

렌더 트리의 각 노드는 DOM 트리의 요소와 연결되며, CSSOM 트리에서 해당 요소에 적용된 스타일 정보를 포함합니다. 즉, 렌더 트리는 HTML 문서의 구조와 각 요소의 스타일 정보를 모두 포함한 트리입니다.


 

네번째로 레이아웃입니다.
렌더 트리가 생성된 후, 브라우저는 이 트리를 사용해 각 요소의 정확한 위치와 크기를 계산합니다. 이 과정이 바로 레이아웃(Layout)입니다. 레이아웃 과정에서는 렌더 트리의 각 노드가 화면의 어디에 위치할지, 그리고 얼마나 큰지를 계산하게 됩니다.

이 계산은 화면의 뷰포트(viewport) 크기와 같은 정보에 의존합니다. 예를 들어, 화면 크기가 변경되면 브라우저는 레이아웃 과정을 다시 수행해야 합니다. 이 과정을 흔히 '리플로우(Reflow)'라고 부르는데, 리플로우는 성능에 영향을 줄 수 있으므로 이를 최소화하는 것이 중요합니다.


 

다섯번째로 페인팅입니다.
레이아웃이 완료되면, 브라우저는 각 요소를 실제로 화면에 그리는 작업을 시작합니다. 이 과정을 페인팅이라고 합니다. 페인팅 단계에서는 텍스트, 색상, 그림자, 이미지 등 모든 시각적 요소가 화면에 그려집니다.

페인팅은 화면에 표시될 그래픽 요소를 생성하는 과정이기 때문에, 이 과정도 성능에 큰 영향을 줄 수 있습니다. 특히, 복잡한 그래픽이나 애니메이션이 포함된 경우 페인트 작업이 많아져 성능이 저하될 수 있습니다.


 

마지막 단계는 컴포지팅입니다.
브라우저는 화면에 그려질 요소들을 각각의 레이어(layer)로 분리하고, 이 레이어들을 결합하여 최종 화면을 구성합니다. 이 과정에서는 GPU를 활용하여 각 레이어를 빠르게 합성합니다.

transform과 opacity와 같은 속성은 레이아웃이나 페인트 과정을 거치지 않고, 이 컴포지팅 단계에서만 처리됩니다. 이 덕분에 이러한 속성을 사용하는 애니메이션은 더 부드럽고 빠르게 실행될 수 있습니다. 컴포지팅 단계는 GPU 가속을 활용하여 성능을 최적화하고, 화면에 최종적으로 표시되는 결과를 빠르게 생성하는 데 중요한 역할을 합니다.

 

728x90

리액트에서 성능 최적화를 위해 적용할 수 있는 방법들을 설명해주세요.

 

리액트에서 성능 최적화를 위해 여러 가지 방법을 사용할 수 있는데요. 대표적으로 메모이제이션을 말씀 드릴 수 있겠습니다.

리액트의 memo를 사용하여 컴포넌트를 메모이제이션할 수 있습니다. 이는 컴포넌트의 props가 변경되지 않았을 때, 리렌더링을 방지하여 성능을 최적화합니다. 이는 특히 렌더링 비용이 큰 컴포넌트에서 유용합니다.

 

또한 useCallback과 useMemo를 활용할 수도 있습니다.

useCallback 은 함수를 메모이제이션하여 불필요한 함수 재생성을 방지하고, useMemo는 값의 재계산을 방지하여 성능을 최적화합니다. 이를 통해 자식 컴포넌트로 전달되는 함수나 값이 변경되지 않으면 리렌더링을 피할 수 있습니다.

 

마지막으로 코드 스플리팅을 활용해볼 수 있습니다. 코드 스플리팅은 큰 애플리케이션을 여러 개의 작은 청크로 나누어, 필요한 청크만 로드하게 하여 초기 로드 시간을 줄입니다. React.lazy와 Suspense를 사용하여 동적으로 컴포넌트를 로드할 수 있습니다.

 

코드 스플리팅은 어떤 경우에 사용해야 할까요? 🤔

첫번째로는 초기 로딩 시간이 길어지는 경우입니다. 애플리케이션이 커지면, 초기 로딩에 모든 코드를 로드하는 것이 비효율적일 수 있습니다. 코드 스플리팅을 사용해 초기 로드 시 필요한 핵심 코드만 로드하고, 이후 추가적인 기능은 필요할 때 로드하도록 하면 초기 로딩 속도를 크게 개선할 수 있습니다.

 

두번째로는 라우트별 코드 분할이 필요한 경우입니다. SPA에서는 각 페이지가 별도의 기능과 UI를 가지므로, 라우트별로 필요한 코드만 분리하여 로드할 수 있습니다. 이 방식은 리액트의 React.lazy와 Suspense를 사용하여 라우트별 컴포넌트를 동적으로 불러올 때 유용합니다.

 


 

리액트 성능 최적화 방법: 초보자를 위한 가이드

리액트(React)는 사용자 인터페이스를 구축하기 위한 강력한 자바스크립트 라이브러리입니다. 하지만 애플리케이션이 커지고 복잡해질수록 성능 문제가 발생할 수 있습니다. 이 글에서는 리액트 애플리케이션의 성능을 최적화하는 다양한 방법을 초보자도 이해하기 쉽게 설명해 드리겠습니다.

1. 메모이제이션을 활용한 최적화

메모이제이션은 비용이 많이 드는 함수 호출의 결과를 저장하고, 동일한 입력이 다시 발생할 때 재계산 없이 저장된 결과를 반환하는 기술입니다. 

React.memo

React.memo는 컴포넌트를 메모이제이션하여 props가 변경되지 않으면 리렌더링을 방지합니다.

import React from 'react';

const ExpensiveComponent = ({ name, count }) => {
  console.log('ExpensiveComponent 렌더링:', name);
  
  // 비용이 많이 드는 렌더링 작업 가정
  const items = [];
  for (let i = 0; i < count; i++) {
    items.push(<div key={i}>Item {i}</div>);
  }
  
  return (
    <div>
      <h2>Hello, {name}!</h2>
      {items}
    </div>
  );
};

// React.memo로 컴포넌트 감싸기
export default React.memo(ExpensiveComponent);

이제 name이나 count props가 변경되지 않는 한, 부모 컴포넌트가 리렌더링되더라도 ExpensiveComponent는 다시 렌더링되지 않습니다.

 

useCallback

함수를 메모이제이션하여 불필요한 재생성을 방지합니다. 특히 자식 컴포넌트에 함수를 props로 전달할 때 유용합니다.

import React, { useState, useCallback } from 'react';
import ExpensiveButton from './ExpensiveButton';

const ParentComponent = () => {
  const [count, setCount] = useState(0);
  
  // useCallback 없이 매 렌더링마다 새로운 함수 생성
  const badHandleClick = () => {
    console.log('버튼 클릭됨');
  };
  
  // useCallback을 사용하여 함수 메모이제이션
  const goodHandleClick = useCallback(() => {
    console.log('버튼 클릭됨');
  }, []); // 의존성 배열이 비어있으므로 함수는 한 번만 생성됨
  
  return (
    <div>
      <h1>카운트: {count}</h1>
      <button onClick={() => setCount(count + 1)}>카운트 증가</button>
      
      {/* 매 렌더링마다 재생성되는 함수를 전달하여 불필요한 리렌더링 발생 */}
      <ExpensiveButton onClick={badHandleClick} label="좋지 않은 예" />
      
      {/* 메모이제이션된 함수를 전달하여 불필요한 리렌더링 방지 */}
      <ExpensiveButton onClick={goodHandleClick} label="좋은 예" />
    </div>
  );
};

// ExpensiveButton.js
import React from 'react';

const ExpensiveButton = ({ onClick, label }) => {
  console.log(`${label} 버튼 렌더링`);
  return <button onClick={onClick}>{label}</button>;
};

export default React.memo(ExpensiveButton);

useCallback 훅을 사용하면 의존성 배열의 값이 변경되지 않는 한 동일한 함수 인스턴스를 재사용합니다. 이렇게 하면 React.memo로 감싼 자식 컴포넌트의 불필요한 리렌더링을 방지할 수 있습니다.

 

useMemo

계산 비용이 큰 값을 메모이제이션하여 불필요한 재계산을 방지합니다.

import React, { useState, useMemo } from 'react';

const ExpensiveCalculation = ({ numbers }) => {
  // 비용이 많이 드는 계산 (예: 소수 찾기)
  const findPrimes = (nums) => {
    console.log('소수 계산 중...');
    return nums.filter(num => {
      if (num <= 1) return false;
      if (num <= 3) return true;
      
      if (num % 2 === 0 || num % 3 === 0) return false;
      
      for (let i = 5; i * i <= num; i += 6) {
        if (num % i === 0 || num % (i + 2) === 0) return false;
      }
      
      return true;
    });
  };
  
  // useMemo 없이 매 렌더링마다 재계산
  // const primes = findPrimes(numbers);
  
  // useMemo를 사용하여 numbers가 변경될 때만 재계산
  const primes = useMemo(() => findPrimes(numbers), [numbers]);
  
  return (
    <div>
      <h2>소수 목록:</h2>
      <ul>
        {primes.map(prime => <li key={prime}>{prime}</li>)}
      </ul>
    </div>
  );
};

useMemo는 종속성 배열의 값이 변경될 때만 값을 다시 계산합니다. 이를 통해 계산 비용이 큰 연산의 불필요한 반복을 방지할 수 있습니다.

 

2. 불필요한 렌더링 최적화

상태 업데이트 최적화

상태 업데이트 방식에 따라 불필요한 렌더링이 발생할 수 있습니다. 다음과 같은 방법으로 최적화할 수 있습니다.

함수형 업데이트 사용

// 좋지 않은 예
const [count, setCount] = useState(0);
const increment = () => {
  setCount(count + 1);
  setCount(count + 1); // 실제로는 한 번만 증가
};

// 좋은 예
const [count, setCount] = useState(0);
const increment = () => {
  setCount(prevCount => prevCount + 1);
  setCount(prevCount => prevCount + 1); // 두 번 증가
};

객체 상태 업데이트 최적화

// 좋지 않은 예
const [user, setUser] = useState({ name: '홍길동', age: 30 });
const updateName = (newName) => {
  // 매번 새 객체를 생성하여 불필요한 렌더링 발생 가능성
  setUser({ name: newName, age: user.age });
};

// 좋은 예
const updateName = (newName) => {
  // 스프레드 연산자를 사용하여 이전 상태 유지
  setUser(prevUser => ({ ...prevUser, name: newName }));
};

컴포넌트 분할

큰 컴포넌트를 작은 컴포넌트로 분할하면 상태 변경 시 필요한 부분만 리렌더링할 수 있습니다.

// 좋지 않은 예: 하나의 큰 컴포넌트
const UserProfile = () => {
  const [user, setUser] = useState({ name: '홍길동', age: 30 });
  const [posts, setPosts] = useState([]);
  
  // user나 posts 상태가 변경되면 전체 컴포넌트가 리렌더링됨
  return (
    <div>
      <h1>{user.name}의 프로필</h1>
      <p>나이: {user.age}</p>
      <div>
        <h2>작성한 글</h2>
        {posts.map(post => (
          <div key={post.id}>
            <h3>{post.title}</h3>
            <p>{post.content}</p>
          </div>
        ))}
      </div>
    </div>
  );
};

// 좋은 예: 작은 컴포넌트로 분할
const UserInfo = React.memo(({ user }) => {
  return (
    <div>
      <h1>{user.name}의 프로필</h1>
      <p>나이: {user.age}</p>
    </div>
  );
});

const UserPosts = React.memo(({ posts }) => {
  return (
    <div>
      <h2>작성한 글</h2>
      {posts.map(post => (
        <div key={post.id}>
          <h3>{post.title}</h3>
          <p>{post.content}</p>
        </div>
      ))}
    </div>
  );
});

const UserProfile = () => {
  const [user, setUser] = useState({ name: '홍길동', age: 30 });
  const [posts, setPosts] = useState([]);
  
  // user가 변경되면 UserInfo만 리렌더링
  // posts가 변경되면 UserPosts만 리렌더링
  return (
    <div>
      <UserInfo user={user} />
      <UserPosts posts={posts} />
    </div>
  );
};

3. 코드 스플리팅(Code Splitting)

코드 스플리팅은 애플리케이션을 여러 개의 작은 청크로 나누어 필요할 때만 로드하는 기술입니다. 이를 통해 초기 로딩 시간을 크게 줄일 수 있습니다.

React.lazy와 Suspense

import React, { Suspense, lazy } from 'react';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import Loading from './Loading';

// 동적으로 컴포넌트 import
const Home = lazy(() => import('./routes/Home'));
const About = lazy(() => import('./routes/About'));
const UserProfile = lazy(() => import('./routes/UserProfile'));

const App = () => {
  return (
    <Router>
      <Suspense fallback={<Loading />}>
        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="/about" element={<About />} />
          <Route path="/profile/:id" element={<UserProfile />} />
        </Routes>
      </Suspense>
    </Router>
  );
};

이 예제에서는 React.lazy를 사용하여 각 라우트 컴포넌트를 동적으로 로드합니다. Suspense 컴포넌트는 lazy 컴포넌트가 로드되는 동안 폴백 UI(예: 로딩 스피너)를 표시합니다.

 

코드 스플리팅은 언제 사용해야 할까?

1. 초기 로딩 시간이 길어지는 경우

대규모 애플리케이션에서는 초기에 모든 코드를 로드하면 사용자가 첫 화면을 보기까지 오랜 시간이 걸릴 수 있습니다. 코드 스플리팅을 사용하면 필요한 핵심 기능만 먼저 로드하고, 나머지는 필요할 때 로드할 수 있습니다.

// 조건부로 컴포넌트 로드하기
import React, { useState, lazy, Suspense } from 'react';

const LargeDataChart = lazy(() => import('./LargeDataChart'));

const Dashboard = () => {
  const [showChart, setShowChart] = useState(false);
  
  return (
    <div>
      <h1>대시보드</h1>
      <button onClick={() => setShowChart(!showChart)}>
        {showChart ? '차트 숨기기' : '차트 보기'}
      </button>
      
      {showChart && (
        <Suspense fallback={<div>차트 로딩 중...</div>}>
          <LargeDataChart />
        </Suspense>
      )}
    </div>
  );
};

이 예제에서는 사용자가 "차트 보기" 버튼을 클릭할 때만 차트 컴포넌트를 로드합니다. 이렇게 하면 초기 페이지 로드 시간을 크게 줄일 수 있습니다.

2. 라우트별 코드 분할이 필요한 경우

단일 페이지 애플리케이션(SPA)에서는 각 페이지가 서로 다른 기능과 UI를 가질 수 있습니다. 라우트별로 코드를 분할하면 사용자가 방문한 페이지의 코드만 로드하여 효율성을 높일 수 있습니다.

앞서 본 라우팅 예제와 같이 React.lazy와 Suspense를 사용하여 각 라우트의 컴포넌트를 필요할 때만 로드할 수 있습니다.

 

4. 가상화(Virtualization) 기법

대량의 데이터를 표시할 때는 가상화 기법을 사용하여 화면에 보이는 항목만 렌더링할 수 있습니다. react-window나 react-virtualized 같은 라이브러리를 사용하면 됩니다.

import React from 'react';
import { FixedSizeList } from 'react-window';

const VirtualizedList = ({ items }) => {
  const Row = ({ index, style }) => (
    <div style={style}>
      <p>Item {items[index]}</p>
    </div>
  );

  return (
    <FixedSizeList
      height={400}
      width={300}
      itemCount={items.length}
      itemSize={50}
    >
      {Row}
    </FixedSizeList>
  );
};

// 사용 예
const LargeList = () => {
  // 10,000개의 항목이 있는 배열 생성
  const items = Array.from({ length: 10000 }, (_, i) => i);
  
  return <VirtualizedList items={items} />;
};

이 예제에서는 10,000개의 항목이 있지만, 실제로는 화면에 보이는 항목만 렌더링되므로 성능이 크게 향상됩니다.

 

5. 웹 워커(Web Worker) 활용

무거운 계산은 메인 스레드가 아닌 웹 워커에서 수행하여 UI 렌더링을 방해하지 않도록 할 수 있습니다.

// worker.js
self.addEventListener('message', e => {
  if (e.data.command === 'calculate') {
    // 무거운 계산 수행
    const result = performHeavyCalculation(e.data.numbers);
    self.postMessage({ result });
  }
});

function performHeavyCalculation(numbers) {
  // 무거운 계산 로직
  return numbers.reduce((sum, num) => sum + Math.pow(num, 2), 0);
}

// React 컴포넌트
import React, { useState, useEffect } from 'react';

const HeavyCalculationComponent = ({ numbers }) => {
  const [result, setResult] = useState(null);
  const [calculating, setCalculating] = useState(false);
  
  useEffect(() => {
    const worker = new Worker('./worker.js');
    
    worker.onmessage = e => {
      setResult(e.data.result);
      setCalculating(false);
    };
    
    setCalculating(true);
    worker.postMessage({
      command: 'calculate',
      numbers
    });
    
    return () => worker.terminate();
  }, [numbers]);
  
  return (
    <div>
      <h2>계산 결과</h2>
      {calculating ? <p>계산 중...</p> : <p>결과: {result}</p>}
    </div>
  );
};

웹 워커를 사용하면 복잡한 계산을 백그라운드에서 처리하여 UI의 반응성을 유지할 수 있습니다.

결론

리액트 애플리케이션의 성능을 최적화하는 방법은 다양합니다. 메모이제이션(React.memo, useCallback, useMemo), 불필요한 렌더링 방지, 코드 스플리팅, 가상화, 웹 워커 등의 기법을 상황에 맞게 적절히 활용하면 사용자 경험을 크게 향상시킬 수 있습니다.

하지만 성능 최적화는 항상 필요한 것은 아닙니다. "조기 최적화는 모든 악의 근원"이라는 말이 있듯이, 실제로 성능 문제가 발생했을 때 적절한 측정과 분석을 통해 최적화하는 것이 중요합니다. React DevTools의 Profiler를 사용하여 성능 병목 현상을 식별하고, 필요한 부분만 선택적으로 최적화하는 것이 좋은 접근 방식입니다.

최적화는 지속적인 과정이며, 애플리케이션의 특성과 사용자의 요구에 맞게 적절한 전략을 선택해야 합니다.

728x90

1. PK(Primary Key)란?
PK는 테이블에서 각 행(row)을 고유하게 식별할 수 있는 열(column)입니다. 쉽게 말해서 주민등록번호나 학번처럼 절대로 중복되지 않는 고유한 값이에요.

 

PK의 특징
- 중복될 수 없음
- NULL 값을 가질 수 없음
- 테이블당 하나만 존재

2. 인덱스(Index)란?
인덱스는 데이터베이스 테이블의 검색 속도를 향상시키기 위한 자료구조입니다. 책의 목차나 사전의 색인과 같은 역할을 한다고 생각하면 됩니다.

 

인덱스의 특징
검색 속도가 빨라짐
데이터 입력/수정/삭제 시 약간의 성능 저하가 있음
추가 저장공간이 필요함

3. PK와 인덱스의 관계
중요: PK로 지정된 열은 자동으로 인덱스가 생성됩니다!
실생활 예시로 이해하기 🏫


학생 정보 테이블 예시:
CREATE TABLE students (
    student_id INT PRIMARY KEY,  -- PK로 지정
    name VARCHAR(50),
    grade INT
);

이를 실생활에 비유하면
학생증(PK)
모든 학생은 고유한 학번을 가짐
절대 중복될 수 없음
반드시 가지고 있어야 함

학생 명부(인덱스)
학번 순으로 정렬되어 있음
특정 학생을 빨리 찾을 수 있음
명부가 있으면 전체 학생부를 뒤지지 않아도 됨

검색 예시
SELECT * FROM students WHERE student_id = 12345;

PK로 검색할 때: 인덱스를 통해 바로 찾을 수 있음 (매우 빠름)
인덱스 없는 일반 열로 검색할 때: 모든 데이터를 하나씩 확인해야 함 (느림)

4. 정리
PK는 각 행의 고유한 식별자
인덱스는 검색 속도를 높이기 위한 도구
PK는 자동으로 인덱스가 생성됨
PK는 테이블당 하나, 인덱스는 여러 개 가능

PK는 우리의 주민등록번호, 인덱스는 전화번호부라고 생각하면 됩니다. 주민등록번호로 사람을 찾을 때는 자동으로 정리된 목록(인덱스)이 있어서 빨리 찾을 수 있죠!

728x90

데이터베이스 인덱스에 대해서 설명해주세요.

인덱스는 데이터베이스 테이블의 검색 속도를 향상시키기 위한 자료구조로 백과사전의 색인과 같습니다. 저장되는 컬럼의 값을 사용하여 항상 정렬된 상태를 유지하는 것이 특징입니다. 이러한 특징으로 인해 인덱스는 INSERT, UPDATE, DELETE의 성능이 희생된다는 것이 단점입니다. (검색이 장점)

 

인덱스는 어떤 자료 구조로 이루어져있나요? 🤔

MySQL InnoDB를 기준으로 설명드리자면, B+Tree와 같은 변형 B-Tree 자료구조를 이용해서 인덱스를 구현합니다. 기본 토대는 B-Tree 인덱스이기 때문에 이를 기준으로 설명합니다. B-Tree 인덱스는 컬럼의 값을 변형하지 않고 인덱스 구조체 내에서 항상 정렬된 상태로 유지합니다.

B-Tree(Balanced-Tree)에서는 크게 3가지 노드가 존재합니다. 최상위에 하나의 루트 노드가 존재하며, 가장 하위 노드인 리프 노드가 존재합니다. 이 두 노드의 중간에 존재하는 브랜치 노드가 존재합니다. 최하위 노드인 리프 노드에는 실제 데이터 레코드를 찾아가기 위한 주소값을 가지고 있습니다.

InnoDB 스토리지 엔진에서는 세컨더리 인덱스(프라이머리 인덱스를 제외한 모든 인덱스)의 리프 노드에는 레코드의 PK가 저장됩니다. 따라서 세컨더리 인덱스 검색에서는 레코드를 읽기 위해 PK를 가지고 있는 B-Tree를 다시 한번 검색해야합니다.

 

MySQL 스캔 방식은 어떤 게 있나요? 😀

MySQL에는 크게 인덱스 레인지 스캔, 인덱스 풀 스캔, 루스 인덱스 스캔 방식이 있습니다.

인덱스 레인지 스캔은 검색할 인덱스 범위가 결정되었을 경우 사용하며 가장 빠릅니다.

  • 인덱스에서 조건을 만족하는 값이 저장된 시작 리프 노드를 찾습니다.(index seek)
  • 시작 리프 노드부터 필요한 만큼 인덱스를 차례대로 읽습니다. (index scan)
  • 인덱스 키와 레코드 주소를 이용해 저장된 페이지를 가져오고 레코드를 읽어옵니다.

레코드를 읽어오는 과정에서 랜덤 IO가 발생할 수 있습니다. 읽어야할 데이터 레코드가 전체 20-25%의 경우에는 풀 테이블 스캔(순차 IO를 이용)이 더욱 좋을 수 있습니다.

인덱스 풀 스캔은 인덱스를 사용하지만 인덱스를 처음부터 끝까지 모두 읽는 방식입니다.

  • 인덱스를 ABC 순서로 만들었는데 조건절에 B 혹은 C로 검색하는 경우 사용됩니다.
  • 인덱스를 생성하는 목적은 아니지만, 그래도 풀 테이블 스캔보다는 낫습니다. (데이터 레코드까지 읽지 않는 경우)

루스 인덱스 스캔은 듬성듬성하게 인덱스를 읽는 것을 의미합니다. (앞서 언급한 인덱스 레인지, 인덱스 풀 스캔은 타이트 인덱스 스캔으로 분류됩니다.)

  • 중간에 필요하지 않은 인덱스 키 값은 무시하고 다음으로 넘어가는 형태로 처리합니다.
  • group by, max(), min() 함수에 대해 최적화하는 경우에 사용됩니다.

 

[ 인덱스(index)의 장점과 단점 ]

    • 장점
      • 테이블을 조회하는 속도와 그에 따른 성능을 향상시킬 수 있다.
      • 전반적인 시스템의 부하를 줄일 수 있다.
    • 단점
      • 인덱스를 관리하기 위해 DB의 약 10%에 해당하는 저장공간이 필요하다.
      • 인덱스를 관리하기 위해 추가 작업이 필요하다.
      • 인덱스를 잘못 사용할 경우 오히려 성능이 저하되는 역효과가 발생할 수 있다.

[ 인덱스(index)를 사용하면 좋은 경우 ]

    • 규모가 작지 않은 테이블
    • INSERT, UPDATE, DELETE가 자주 발생하지 않는 컬럼
    • JOIN이나 WHERE 또는 ORDER BY에 자주 사용되는 컬럼
    • 데이터의 중복도가 낮은 컬럼
    • 기타 등등

출처: https://mangkyu.tistory.com/96 [MangKyu's Diary:티스토리]

728x90

리액트의 Controlled Component와 Uncontrolled Component의 차이점에 대해서 설명해주세요.

Controlled Component는 리액트 상태(state)를 통해 입력 값을 제어하는 컴포넌트를 말합니다. 이 방식에서는 입력 요소의 값(value)을 리액트 상태와 동기화하고, 사용자가 입력을 변경할 때마다 onChange 이벤트 핸들러를 통해 상태를 업데이트합니다. Controlled Component는 값이 리액트의 state로 관리되므로, 입력 시마다 값을 검증하거나, 값을 자유롭게 변경할 수 있으며, 복잡한 폼 로직을 처리하는 데 유용합니다.

 

Uncontrolled Component는 입력 값을 리액트의 상태로 관리하지 않고, DOM을 통해 입력 값을 제어하는 방식입니다. 즉, 입력 요소의 값은 DOM에서 직접 관리되며, 리액트는 이를 제어하지 않습니다. 이 방식에서는 useRef를 사용해 생성된 참조 객체인 ref를 사용하여 DOM 요소에 직접 접근하여 값을 읽거나 조작합니다. Uncontrolled Component는 리액트 상태 관리에 따른 성능 비용이 없으므로 상대적으로 간단한 폼에서 주로 사용됩니다.

Controlled Component와 Uncontrolled Component는 각각 어떤 상황에서 사용되나요? 🤔

단순한 입력 필드가 포함된 폼에서는 입력 요소의 값을 리액트 상태로 관리할 필요성이 적으므로, Uncontrolled Component를 사용하는 것이 더 간단하고 성능이 좋습니다. 사용자가 제출 버튼을 클릭했을 때만 입력 값을 가져와도 충분한 경우를 예시로 들 수 있습니다.

반면, 값을 입력할 때마다 유효성 검증을 실시간으로 해주어야 하는 경우에는 Controlled Component를 사용해야 합니다.

 


 

리액트의 Controlled Component와 Uncontrolled Component 

1. 폼 관리의 두 가지 방식

React에서 사용자 입력을 처리하는 폼을 구현할 때, 두 가지 주요 접근 방식이 있습니다: Controlled Components(제어 컴포넌트)Uncontrolled Components(비제어 컴포넌트). 이 두 방식은 폼 데이터를 어떻게 관리하는지에 따라 구분됩니다. 초보자도 쉽게 이해할 수 있도록 각각의 개념과 차이점, 그리고 언제 어떤 방식을 사용해야 하는지 알아보겠습니다.

 

2. Controlled Component란?

2.1 특징과 작동 방식

Controlled Component는 React의 상태(state)를 "진리의 유일한 원천(Single Source of Truth)"으로 사용하는 방식입니다. 쉽게 말해, 입력 요소(input, textarea, select 등)의 값을 React가 완전히 제어하는 것입니다.

이 방식의 핵심 특징은 다음과 같습니다:

  • 모든 폼 데이터는 React의 상태(state)에 저장됩니다.
  • 입력 요소의 값(value)은 항상 React 상태와 연결됩니다.
  • 사용자가 입력할 때마다 onChange 이벤트 핸들러가 호출되어 상태를 업데이트합니다.

이러한 방식을 "제어(Controlled)"라고 부르는 이유는 React가 입력 요소의 값을 적극적으로 제어하기 때문입니다.

2.2 코드 예시

아래는 간단한 Controlled Component 예시입니다:

import React, { useState } from 'react';

function ControlledForm() {
  // React 상태로 입력 값 관리
  const [name, setName] = useState('');
  
  const handleChange = (event) => {
    setName(event.target.value);
  };
  
  const handleSubmit = (event) => {
    event.preventDefault();
    alert('입력한 이름: ' + name);
  };
  
  return (
    <form onSubmit={handleSubmit}>
      <label>
        이름:
        <input 
          type="text"
          value={name} // React 상태를 value로 설정
          onChange={handleChange} // 입력 변경 시 상태 업데이트
        />
      </label>
      <button type="submit">제출</button>
    </form>
  );
}

이 예시에서 input 요소의 value는 항상 React의 name 상태와 동일합니다. 사용자가 입력할 때마다 handleChange 함수가 호출되어 상태를 업데이트하고, 이것이 다시 input의 값으로 반영됩니다.

2.3 장점과 사용 시나리오

Controlled Component의 주요 장점은 다음과 같습니다:

  1. 실시간 입력 검증: 사용자가 타이핑할 때마다 입력 값을 검증하고 피드백을 제공할 수 있습니다.
  2. 조건부 제출 버튼 활성화: 폼이 유효할 때만 제출 버튼을 활성화할 수 있습니다.
  3. 입력 값 형식 지정: 예를 들어, 전화번호에 자동으로 하이픈을 추가하는 등의 형식 지정이 가능합니다.
  4. 동적 입력 필드: 하나의 입력이 다른 입력에 영향을 미치는 등의 복잡한 폼 로직을 구현할 수 있습니다.

Controlled Component는 다음과 같은 상황에서 특히 유용합니다:

  • 실시간 유효성 검사가 필요한 폼
  • 다른 UI 요소에 의존하는 입력 필드가 있는 폼
  • 동적으로 변하는 복잡한 폼
  • 제출 전에 입력 값을 처리해야 하는 경우

 

3. Uncontrolled Component란?

3.1 특징과 작동 방식

Uncontrolled Component는 폼 데이터를 React 상태가 아닌 DOM 자체에서 관리하는 방식입니다. 이 방식에서는 React가 입력 요소의 값을 직접 제어하지 않으며, 대신 ref를 사용하여 필요할 때 DOM에서 값을 가져옵니다.

이 방식의 핵심 특징은 다음과 같습니다:

  • 입력 값은 DOM에 의해 관리됩니다.
  • React의 상태(state)를 사용하지 않습니다.
  • ref를 사용하여 DOM 노드에 접근하고 값을 읽습니다.
  • 폼 제출 시에만 입력 값을 가져오는 경우가 많습니다.

3.2 코드 예시

아래는 간단한 Uncontrolled Component 예시입니다:

import React, { useRef } from 'react';

function UncontrolledForm() {
  // ref를 사용하여 DOM 노드에 접근
  const nameInputRef = useRef(null);
  
  const handleSubmit = (event) => {
    event.preventDefault();
    // 제출 시 ref를 통해 입력 값 가져오기
    alert('입력한 이름: ' + nameInputRef.current.value);
  };
  
  return (
    <form onSubmit={handleSubmit}>
      <label>
        이름:
        <input 
          type="text" 
          ref={nameInputRef} // DOM 노드에 대한 참조 설정
          defaultValue="" // 초기값 설정 (선택 사항)
        />
      </label>
      <button type="submit">제출</button>
    </form>
  );
}

이 예시에서는 React의 상태를 사용하지 않고, useRef 훅을 통해 input 요소에 직접 접근합니다. 사용자가 폼을 제출할 때 nameInputRef.current.value를 통해 현재 입력된 값을 가져옵니다.

3.3 장점과 사용 시나리오

Uncontrolled Component의 주요 장점은 다음과 같습니다:

  1. 간단한 구현: 적은 코드로 구현할 수 있어 단순한 폼에 적합합니다.
  2. 성능: 입력할 때마다 렌더링이 발생하지 않아 성능 면에서 효율적일 수 있습니다.
  3. 기존 코드 통합: 기존의 비-React 코드나 라이브러리와 통합하기 쉽습니다.
  4. 파일 입력 관리: <input type="file"> 같은 요소는 Uncontrolled 방식으로만 다룰 수 있습니다.

Uncontrolled Component는 다음과 같은 상황에서 주로 사용됩니다:

  • 단순한 폼(예: 간단한 로그인 폼)
  • 제출 시에만 값을 확인하면 되는 경우
  • 파일 업로드와 같이 제어하기 어려운 입력이 포함된 경우
  • React 외부 라이브러리와의 통합

 

4. 비교: 언제 어떤 방식을 사용해야 할까?

Controlled Component와 Uncontrolled Component의 주요 차이점을 비교해보겠습니다:

특성 Controlled Component Uncontrolled Component

데이터 관리 React 상태(state) DOM
코드 복잡성 상대적으로 많은 코드 필요 적은 코드로 구현 가능
실시간 유효성 검사 쉽게 구현 가능 구현하기 어려움
조건부 렌더링 상태에 기반하여 쉽게 구현 추가 작업 필요
초기값 설정 value prop defaultValue prop
성능 타이핑마다 리렌더링 발생 가능 리렌더링 발생하지 않음
사용 사례 복잡한 폼, 실시간 검증 필요 간단한 폼, 제출 시에만 검증

선택 기준

  • Controlled Component를 선택해야 할 때: 실시간 유효성 검사, 조건부 제출, 동적 입력 필드 등 복잡한 폼 로직이 필요한 경우
  • Uncontrolled Component를 선택해야 할 때: 간단한 폼, 성능이 중요한 경우, 기존 코드와의 통합이 필요한 경우

5. 실전 예제: 회원가입 폼

두 가지 방식의 차이를 더 명확히 이해하기 위해 간단한 회원가입 폼을 두 방식으로 구현해보겠습니다.

5.1 Controlled 방식

import React, { useState } from 'react';

function ControlledSignupForm() {
  const [formData, setFormData] = useState({
    username: '',
    email: '',
    password: '',
    confirmPassword: ''
  });
  
  const [errors, setErrors] = useState({});
  
  // 입력 변경 처리
  const handleChange = (e) => {
    const { name, value } = e.target;
    setFormData({
      ...formData,
      [name]: value
    });
    
    // 실시간 유효성 검사
    validateField(name, value);
  };
  
  // 특정 필드 유효성 검사
  const validateField = (name, value) => {
    let tempErrors = { ...errors };
    
    switch (name) {
      case 'username':
        tempErrors.username = value.length < 3 ? '사용자 이름은 3자 이상이어야 합니다.' : '';
        break;
      case 'email':
        const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
        tempErrors.email = !emailRegex.test(value) ? '유효한 이메일 주소를 입력하세요.' : '';
        break;
      case 'password':
        tempErrors.password = value.length < 6 ? '비밀번호는 6자 이상이어야 합니다.' : '';
        
        // 비밀번호 확인 필드도 함께 검사
        if (formData.confirmPassword) {
          tempErrors.confirmPassword = 
            value !== formData.confirmPassword ? '비밀번호가 일치하지 않습니다.' : '';
        }
        break;
      case 'confirmPassword':
        tempErrors.confirmPassword = 
          value !== formData.password ? '비밀번호가 일치하지 않습니다.' : '';
        break;
      default:
        break;
    }
    
    setErrors(tempErrors);
  };
  
  // 폼 제출 처리
  const handleSubmit = (e) => {
    e.preventDefault();
    
    // 모든 필드 유효성 검사
    let isValid = true;
    let tempErrors = { ...errors };
    
    // 각 필드 검사
    Object.keys(formData).forEach(key => {
      if (!formData[key]) {
        tempErrors[key] = '이 필드는 필수입니다.';
        isValid = false;
      } else {
        validateField(key, formData[key]);
        if (tempErrors[key]) isValid = false;
      }
    });
    
    setErrors(tempErrors);
    
    if (isValid) {
      // 유효한 데이터로 회원가입 처리
      console.log('회원가입 데이터:', formData);
      alert('회원가입이 완료되었습니다!');
    }
  };
  
  return (
    <form onSubmit={handleSubmit}>
      <h2>Controlled 회원가입 폼</h2>
      
      <div>
        <label>사용자 이름:</label>
        <input
          type="text"
          name="username"
          value={formData.username}
          onChange={handleChange}
        />
        {errors.username && <p className="error">{errors.username}</p>}
      </div>
      
      <div>
        <label>이메일:</label>
        <input
          type="email"
          name="email"
          value={formData.email}
          onChange={handleChange}
        />
        {errors.email && <p className="error">{errors.email}</p>}
      </div>
      
      <div>
        <label>비밀번호:</label>
        <input
          type="password"
          name="password"
          value={formData.password}
          onChange={handleChange}
        />
        {errors.password && <p className="error">{errors.password}</p>}
      </div>
      
      <div>
        <label>비밀번호 확인:</label>
        <input
          type="password"
          name="confirmPassword"
          value={formData.confirmPassword}
          onChange={handleChange}
        />
        {errors.confirmPassword && <p className="error">{errors.confirmPassword}</p>}
      </div>
      
      <button type="submit">회원가입</button>
    </form>
  );
}

이 Controlled 방식의 회원가입 폼은 다음과 같은 장점을 가집니다:

  • 사용자가 입력하는 동안 실시간으로 유효성 검사를 수행합니다.
  • 비밀번호와, 비밀번호 확인 필드를 비교하는 등의 복잡한 검증이 가능합니다.
  • 모든 필드를 하나의 상태 객체로 관리하여 코드 구성이 깔끔합니다.

5.2 Uncontrolled 방식

import React, { useRef } from 'react';

function UncontrolledSignupForm() {
  // 각 입력 필드에 대한 ref 생성
  const usernameRef = useRef(null);
  const emailRef = useRef(null);
  const passwordRef = useRef(null);
  const confirmPasswordRef = useRef(null);
  
  // 에러 메시지 표시용 ref
  const usernameErrorRef = useRef(null);
  const emailErrorRef = useRef(null);
  const passwordErrorRef = useRef(null);
  const confirmPasswordErrorRef = useRef(null);
  
  // 폼 제출 처리
  const handleSubmit = (e) => {
    e.preventDefault();
    
    // 폼 데이터 수집
    const formData = {
      username: usernameRef.current.value,
      email: emailRef.current.value,
      password: passwordRef.current.value,
      confirmPassword: confirmPasswordRef.current.value
    };
    
    // 유효성 검사
    let isValid = true;
    
    // 사용자 이름 검사
    if (!formData.username) {
      usernameErrorRef.current.textContent = '사용자 이름은 필수입니다.';
      isValid = false;
    } else if (formData.username.length < 3) {
      usernameErrorRef.current.textContent = '사용자 이름은 3자 이상이어야 합니다.';
      isValid = false;
    } else {
      usernameErrorRef.current.textContent = '';
    }
    
    // 이메일 검사
    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    if (!formData.email) {
      emailErrorRef.current.textContent = '이메일은 필수입니다.';
      isValid = false;
    } else if (!emailRegex.test(formData.email)) {
      emailErrorRef.current.textContent = '유효한 이메일 주소를 입력하세요.';
      isValid = false;
    } else {
      emailErrorRef.current.textContent = '';
    }
    
    // 비밀번호 검사
    if (!formData.password) {
      passwordErrorRef.current.textContent = '비밀번호는 필수입니다.';
      isValid = false;
    } else if (formData.password.length < 6) {
      passwordErrorRef.current.textContent = '비밀번호는 6자 이상이어야 합니다.';
      isValid = false;
    } else {
      passwordErrorRef.current.textContent = '';
    }
    
    // 비밀번호 확인 검사
    if (!formData.confirmPassword) {
      confirmPasswordErrorRef.current.textContent = '비밀번호 확인은 필수입니다.';
      isValid = false;
    } else if (formData.password !== formData.confirmPassword) {
      confirmPasswordErrorRef.current.textContent = '비밀번호가 일치하지 않습니다.';
      isValid = false;
    } else {
      confirmPasswordErrorRef.current.textContent = '';
    }
    
    if (isValid) {
      // 유효한 데이터로 회원가입 처리
      console.log('회원가입 데이터:', formData);
      alert('회원가입이 완료되었습니다!');
    }
  };
  
  return (
    <form onSubmit={handleSubmit}>
      <h2>Uncontrolled 회원가입 폼</h2>
      
      <div>
        <label>사용자 이름:</label>
        <input
          type="text"
          ref={usernameRef}
          defaultValue=""
        />
        <p className="error" ref={usernameErrorRef}></p>
      </div>
      
      <div>
        <label>이메일:</label>
        <input
          type="email"
          ref={emailRef}
          defaultValue=""
        />
        <p className="error" ref={emailErrorRef}></p>
      </div>
      
      <div>
        <label>비밀번호:</label>
        <input
          type="password"
          ref={passwordRef}
          defaultValue=""
        />
        <p className="error" ref={passwordErrorRef}></p>
      </div>
      
      <div>
        <label>비밀번호 확인:</label>
        <input
          type="password"
          ref={confirmPasswordRef}
          defaultValue=""
        />
        <p className="error" ref={confirmPasswordErrorRef}></p>
      </div>
      
      <button type="submit">회원가입</button>
    </form>
  );
}

이 Uncontrolled 방식의 회원가입 폼은 다음과 같은 특징을 가집니다:

  • React 상태를 사용하지 않고 DOM에서 직접 값을 가져옵니다.
  • 폼 제출 시에만 유효성 검사를 수행합니다.
  • 에러 메시지도 React 상태가 아닌 DOM을 통해 관리합니다.
  • Controlled 방식에 비해 코드가 더 복잡해질 수 있지만, 렌더링 성능은 더 좋을 수 있습니다.

6. 결론

Controlled Component와 Uncontrolled Component는 각각 장단점이 있으며, 상황에 따라 적절한 방식을 선택하는 것이 중요합니다.

Controlled Component는:

  • 실시간 피드백과 유효성 검사가 필요한 복잡한 폼에 적합합니다.
  • React의 데이터 흐름 철학에 더 잘 맞습니다.
  • 폼 상태를 완전히 제어할 수 있어 복잡한 UI 상호작용을 구현하기 쉽습니다.

Uncontrolled Component는:

  • 간단한 폼, 특히 제출 시에만 값을 확인하면 되는 경우에 적합합니다.
  • 코드가 더 간결할 수 있으며, 성능 면에서 이점이 있을 수 있습니다.
  • 파일 입력과 같은 특정 타입의 입력을 처리할 때 필요합니다.

어떤 방식을 선택하든, 일관성을 유지하는 것이 중요합니다. 하나의 폼 내에서 두 방식을 혼합하여 사용하면 코드가 혼란스러워질 수 있으므로, 가능하면 한 가지 방식을 선택하여 일관되게 사용하는 것이 좋습니다.

리액트에서 폼을 다루는 것은 웹 애플리케이션 개발에서 매우 중요한 부분입니다. Controlled Component와 Uncontrolled Component의 개념을 이해하고 적절히 활용함으로써, 더 효율적이고 유지보수하기 쉬운 폼을 구현할 수 있습니다.

728x90

1. Props란 무엇인가?

리액트(React)에서 Props(Properties의 줄임말)부모 컴포넌트가 자식 컴포넌트로 데이터를 전달하는 방법입니다. 쉽게 말해, Props는 컴포넌트 간에 정보를 전달하는 통로입니다.

1.1 Props의 특성

Props의 가장 중요한 특성은 읽기 전용(Read-only)이라는 점입니다. 자식 컴포넌트는 자신이 받은 Props를 직접 수정할 수 없습니다. 이것은 마치 함수의 매개변수와 비슷합니다 - 함수 내부에서 전달받은 인자의 값을 직접 변경하지 않는 것과 같은 원리입니다.

1.2 Props 사용 예시

아래 예시를 통해 Props가 어떻게 사용되는지 살펴보겠습니다:

// 부모 컴포넌트
function ParentComponent() {
  return (
    <ChildComponent name="React" color="blue" />
  );
}

// 자식 컴포넌트
function ChildComponent(props) {
  return (
    <div style={{ color: props.color }}>
      안녕하세요, {props.name}!
    </div>
  );
}

위 예시에서 부모 컴포넌트는 name과 color라는 두 가지 Props를 자식 컴포넌트에 전달하고 있습니다. 자식 컴포넌트는 이 Props를 사용하여 화면에 "안녕하세요, React!"라는 메시지를 파란색으로 표시합니다.

 

2. State란 무엇인가?

State컴포넌트 내부에서 관리되는 데이터입니다. Props가 외부에서 받는 데이터라면, State는 컴포넌트 자신이 소유하고 관리하는 데이터입니다.

2.1 State의 특성

State의 주요 특성은 다음과 같습니다:

  • 컴포넌트 내부에서 선언되고 관리됩니다.
  • 동적으로 변경될 수 있습니다.
  • State가 변경되면 컴포넌트가 다시 렌더링됩니다.
  • 주로 사용자 입력이나 API 응답과 같이 시간에 따라 변할 수 있는 데이터를 관리합니다.

2.2 State 사용 예시

다음은 클릭할 때마다 카운터가 증가하는 간단한 예시입니다:

import React, { useState } from 'react';

function Counter() {
  // count는 상태 변수, setCount는 해당 상태를 변경하는 함수
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>현재 카운트: {count}</p>
      <button onClick={() => setCount(count + 1)}>
        증가
      </button>
    </div>
  );
}

이 예시에서 count는 State로, 초기값은 0입니다. 버튼을 클릭하면 setCount 함수를 호출하여 count 값을 증가시킵니다. State가 변경되면 컴포넌트가 다시 렌더링되어 업데이트된 카운트 값이 화면에 표시됩니다.

 

3. Props가 자식 컴포넌트에서 변하지 않는 이유

Props가 자식 컴포넌트에서 변경되지 않는 이유는 리액트의 단방향 데이터 흐름 원칙 때문입니다.

3.1 단방향 데이터 흐름의 중요성

리액트의 단방향 데이터 흐름은 다음과 같은 장점을 제공합니다:

  1. 예측 가능성: 데이터가 한 방향으로만 흐르기 때문에, 애플리케이션의 상태 변화를 추적하고 이해하기 쉽습니다.
  2. 디버깅 용이성: 문제가 발생했을 때, 데이터의 변화를 추적하기 쉽습니다.
  3. 컴포넌트 재사용성: Props가 불변이면, 컴포넌트는 외부 입력에만 의존하게 되어 재사용이 쉬워집니다.

만약 자식 컴포넌트가 Props를 직접 수정할 수 있다면, 다음과 같은 문제가 발생할 수 있습니다:

// 잘못된 예시
function ChildComponent(props) {
  props.name = "새 이름"; // 오류 발생 가능!
  return <div>{props.name}</div>;
}

이런 코드는 Props의 불변성 원칙을 위반하며, 리액트의 단방향 데이터 흐름을 해치게 됩니다.

 

4. 자식 컴포넌트에서 Props를 변경해야 할 때

그렇다면, 자식 컴포넌트에서 부모로부터 받은 데이터를 변경해야 할 필요가 있다면 어떻게 해야 할까요?

4.1 상태 끌어올리기 패턴

리액트에서는 이런 상황을 상태 끌어올리기(Lifting State Up) 패턴을 통해 해결합니다. 데이터를 변경해야 하는 컴포넌트들의 가장 가까운 공통 부모 컴포넌트에서 상태를 관리하고, 상태 변경 함수를 Props로 전달하는 방식입니다.

4.2 실제 구현 예시

예를 들어, 사용자 이름을 표시하고 수정할 수 있는 컴포넌트를 구현해 보겠습니다:

import React, { useState } from 'react';

// 부모 컴포넌트
function UserProfile() {
  // 상태를 부모 컴포넌트에서 관리
  const [userName, setUserName] = useState("초기 사용자명");

  // 상태 변경 함수를 자식 컴포넌트에 전달
  return (
    <div>
      <h2>사용자 프로필</h2>
      <UserNameDisplay name={userName} />
      <UserNameEditor name={userName} onNameChange={setUserName} />
    </div>
  );
}

// 이름을 표시하는 컴포넌트
function UserNameDisplay({ name }) {
  return <p>현재 사용자: {name}</p>;
}

// 이름을 수정하는 컴포넌트
function UserNameEditor({ name, onNameChange }) {
  return (
    <div>
      <input
        value={name}
        onChange={(e) => onNameChange(e.target.value)}
      />
      <button onClick={() => onNameChange("기본 이름")}>
        기본값으로 초기화
      </button>
    </div>
  );
}

이 예시에서:

  1. UserProfile 컴포넌트는 userName 상태와 이를 변경하는 setUserName 함수를 가지고 있습니다.
  2. UserNameDisplay 컴포넌트는 이름을 표시하기만 합니다.
  3. UserNameEditor 컴포넌트는 이름을 수정할 수 있지만, 직접 Props를 수정하는 대신 부모로부터 받은 onNameChange 함수를 호출하여 변경을 요청합니다.

이렇게 하면 데이터 흐름은 여전히 단방향으로 유지되면서도, 자식 컴포넌트에서 부모 컴포넌트의 상태를 간접적으로 변경할 수 있습니다.

 

5. Props와 State의 효과적인 사용법

Props와 State를 효과적으로 사용하기 위한 몇 가지 팁은 다음과 같습니다:

  1. Props는 설정값으로 생각하세요: Props는 컴포넌트의 설정이나 구성으로 생각하면 좋습니다. 컴포넌트가 어떻게 보여질지, 어떻게 동작할지를 결정합니다.
  2. 상태는 최소한으로 유지하세요: 꼭 필요한 데이터만 State로 관리하고, 가능한 한 적은 상태를 유지하는 것이 좋습니다.
  3. 상태는 적절한 위치에 배치하세요: 특정 State를 필요로 하는 모든 컴포넌트의 가장 가까운 공통 조상에 해당 State를 배치하세요.
  4. 파생된 값은 계산하세요: State에서 계산할 수 있는 값은 별도의 State로 관리하지 말고, 렌더링 과정에서 계산하세요.
// 좋은 예시
function Temperature({ celsius }) {
  // 화씨 온도는 섭씨 온도에서 계산됨
  const fahrenheit = (celsius * 9/5) + 32;
  
  return (
    <div>
      <p>섭씨: {celsius}°C</p>
      <p>화씨: {fahrenheit}°F</p>
    </div>
  );
}

이렇게 Props와 State를 적절히 활용하면, 더 예측 가능하고 관리하기 쉬운 리액트 애플리케이션을 만들 수 있습니다.

Props와 State는 리액트의 기본 개념이지만, 이를 제대로 이해하고 활용하는 것은 효과적인 컴포넌트 설계의 핵심입니다. 컴포넌트가 복잡해질수록 데이터 흐름을 명확하게 유지하는 것이 중요하며, 이를 위해 단방향 데이터 흐름 원칙을 잘 준수해야 합니다.

728x90

🚀 자바스크립트 이벤트 루프: 비동기 작업의 숨겨진 비밀

들어가며: 자바스크립트의 특별한 능력

자바스크립트는 단일 스레드 언어임에도 불구하고, 마치 여러 일을 동시에 처리하는 것처럼 보입니다. 이 마법 같은 능력의 주인공, 바로 이벤트 루프입니다!

 

🤔 이벤트 루프란 무엇인가?

이벤트 루프는 자바스크립트의 비동기 작업을 관리하는 놀라운 메커니즘입니다. 마치 주방장이 여러 요리를 동시에 관리하는 것처럼, 이벤트 루프는 다양한 작업들을 조율합니다.

주요 구성 요소

  1. 콜 스택 (Call Stack)
    • 현재 실행 중인 함수들이 쌓이는 공간
    • 함수 호출 순서를 관리하는 접시 더미라고 생각하세요
  2. 태스크 큐 (Task Queue)
    • 비동기 작업의 결과가 대기하는 공간
    • 두 가지 종류가 있습니다
      • 마이크로태스크 큐
      • 매크로태스크 큐

 

🎬 이벤트 루프의 동작 원리: 실제 예시로 살펴보기

console.log('시작');

setTimeout(() => {
    console.log('타이머 콜백');
}, 0);

Promise.resolve().then(() => {
    console.log('프로미스 콜백');
});

console.log('끝');

이 코드의 실행 순서를 살펴보면:

  1. '시작' 출력
  2. '끝' 출력
  3. '프로미스 콜백' 출력 (마이크로태스크 큐)
  4. '타이머 콜백' 출력 (매크로태스크 큐)

 

왜 이렇게 실행될까요?

  • 마이크로태스크 큐(Promise)가 매크로태스크 큐(setTimeout)보다 먼저 처리됩니다.
  • 동기 코드가 먼저 실행된 후, 비동기 작업이 처리됩니다.

 

🔍 태스크 큐의 종류

1. 마이크로태스크 큐

  • 높은 우선순위
  • Promise, queueMicrotask() 등의 작업 처리
  • 매크로태스크 큐보다 먼저 실행

2. 매크로태스크 큐

  • 상대적으로 낮은 우선순위
  • setTimeout(), setInterval(), requestAnimationFrame() 등
  • 마이크로태스크 큐의 모든 작업이 완료된 후 실행

 

💡 실용적인 예시: 비동기 작업 처리

// 사용자 데이터 fetching 시뮬레이션
function fetchUserData() {
    console.log('사용자 데이터 요청 시작');
    
    // 프로미스를 사용한 비동기 작업
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve({ name: '홍길동', age: 30 });
        }, 1000);
    });
}

console.log('프로그램 시작');

fetchUserData().then((user) => {
    console.log('사용자 데이터:', user);
});

console.log('프로그램 종료');

// 예상 출력:
// 프로그램 시작
// 사용자 데이터 요청 시작
// 프로그램 종료
// 사용자 데이터: { name: '홍길동', age: 30 }

 

🚀 이벤트 루프의 중요성

  1. 비동기 작업 관리: 복잡한 작업을 부드럽게 처리
  2. 성능 최적화: 웹 애플리케이션의 반응성 향상
  3. 논블로킹 작업: 사용자 경험 개선

 

마무리: 이벤트 루프, 그 신비로운 메커니즘

이벤트 루프는 자바스크립트의 비동기 마법을 가능하게 하는 핵심 메커니즘입니다. 복잡해 보이지만, 결국 효율적으로 작업을 관리하는 똑똑한 시스템입니다.

더 깊이 알아보기

  • MDN 웹 문서
  • Jake Archibald의 이벤트 루프 강의
  • 브라우저 개발자 도구로 직접 확인해보기
728x90

여러분, 웹 애플리케이션에서 데이터를 불러올 때 매번 서버에 요청하는 건 비효율적이에요. 그래서 우리에게는 '캐싱'이라는 마법 같은 방법이 있습니다! TanStack Query의 staleTime과 gcTime은 이 마법을 가능하게 하는 핵심 도구입니다.

 

staleTime: 데이터의 신선함을 지키는 시간 🍎

stale (오래되거나 신선하지 않은)

🌟 스토리로 이해하기

상상해보세요. 여러분이 매일 아침 신선한 우유를 냉장고에 보관한다고 가정해봅시다.

  • staleTime은 마치 "이 우유는 2시간 동안은 완전히 신선해!"라고 말하는 것과 같아요.
  • 2시간 동안은 같은 우유를 다시 사러 갈 필요가 없죠.

실제 코드 예시

const { data } = useQuery({
  queryKey: ['products'],
  queryFn: fetchProducts,
  staleTime: 5 * 60 * 1000 // 5분 동안 데이터 신선
})

 

gcTime: 캐시 데이터의 수명 🕰️

gc: 가비지 컬렉션 (Garbage Collection)

🌟 창고 관리자의 비유

창고 관리자가 사용하지 않는 상품을 얼마나 오래 보관할지 결정하는 것과 비슷해요.

  • 특정 상품(데이터)을 더 이상 사용하지 않으면 일정 시간 후 완전히 제거합니다.

실제 코드 예시

const { data } = useQuery({
  queryKey: ['users'],
  queryFn: fetchUsers,
  gcTime: 10 * 60 * 1000 // 10분 후 캐시에서 제거
})

 

실전! 언제 어떻게 사용할까? 🛠️

1. 변하지 않는 데이터

  • 사용자 프로필, 카테고리 목록 등
  • staleTime을 길게 (예: 1시간)
  • gcTime을 더 길게 설정

2. 자주 변하는 데이터

  • 실시간 주식 정보, 채팅 메시지
  • staleTime을 매우 짧게 (0 또는 아주 작은 값)
  • gcTime을 적당히 설정

 

팁! 현명한 캐싱 전략 💡

  • 모든 데이터가 같은 캐싱 전략일 필요는 없어요.
  • 각 데이터의 특성에 맞게 유연하게 설정하세요.

 

마무리: 캐싱은 마법! 🌈

TanStack Query의 staleTime과 gcTime은 여러분의 웹 애플리케이션을 더 빠르고 효율적으로 만들어줄 거예요. 데이터를 현명하게 관리하는 여정, 함께 떠나볼까요? 🚀

728x90

+ Recent posts