🔄 리액트의 Render Phase와 Commit Phase 완전 정복

안녕하세요! 오늘은 리액트(React)가 화면을 그려내는 과정을 쉽게 이해할 수 있도록 render phasecommit phase에 대해 알아보겠습니다. 복잡해 보이는 개념이지만, 일상생활의 예시와 함께 쉽게 설명해드릴게요! 😊

📝 리액트의 렌더링 과정 이해하기

리액트에서 컴포넌트가 화면에 그려지는 과정은 크게 두 단계로 나뉩니다:

  1. Render Phase 🧠: 무엇을 그릴지 '계획'하는 단계
  2. Commit Phase 🎨: 실제로 화면에 '그리는' 단계

이 두 단계를 통해 리액트는 효율적으로 UI를 업데이트하고, 사용자에게 부드러운 경험을 제공합니다.

🧠 Render Phase: 계획 세우기

Render Phase는 리액트가 변경된 상태(state)나 속성(props)에 따라 어떤 UI 요소가 변경되어야 하는지 결정하는 단계입니다.

🏠 일상 속 비유: 집 리모델링 계획

집 리모델링을 생각해봅시다. 실제로 벽을 허물거나 페인트칠을 하기 전에, 먼저 도면을 그리고 어떤 부분을 변경할지 계획하게 됩니다. 이것이 바로 render phase와 같습니다!

✨ Render Phase의 주요 특징

  • 가상 DOM에서만 작업 🌐: 실제 화면(DOM)은 아직 변경되지 않습니다
  • 순수 계산 과정 🧮: 외부 세계에 영향을 주지 않습니다
  • 중단 가능 ⏸️: 필요시 중단했다가 다시 시작할 수 있습니다
  • 비동기적 처리 가능 🔄: React 18의 Concurrent Mode에서는 더 중요한 작업을 위해 렌더링을 잠시 미룰 수 있습니다

💻 코드로 이해하기

function Counter() {
  // 상태 변경이 일어나면
  const [count, setCount] = useState(0);
  
  // 이 부분이 render phase에서 실행됩니다
  // 여기서는 실제 DOM이 변경되지 않아요!
  return (
    <div>
      <p>현재 카운트: {count}</p>
      <button onClick={() => setCount(count + 1)}>증가</button>
    </div>
  );
}

 

 

🎨 Commit Phase: 실제로 그리기

Commit Phase는 render phase에서 계산된 변경사항을 실제 DOM에 적용하는 단계입니다.

🏠 일상 속 비유: 실제 리모델링 작업

계획이 완료된 후, 실제로 망치를 들고 벽을 허물거나 페인트칠을 시작하는 것이 commit phase입니다. 이제 실제 변화가 일어납니다!

✨ Commit Phase의 주요 특징

  • 실제 DOM 업데이트 🖥️: 화면에 변경사항이 반영됩니다
  • 중단 불가능 ⚠️: 일단 시작되면 끝까지 실행됩니다
  • 동기적 처리 ⚡: 한 번에 완료됩니다
  • 사이드 이펙트 실행 🔄: useEffect, useLayoutEffect 등의 훅이 실행됩니다

💻 코드로 이해하기

function ProfileCard({ user }) {
  // render phase에서 계산 후
  
  useEffect(() => {
    // commit phase가 완료된 후 실행됩니다
    console.log('프로필이 화면에 그려졌습니다!');
    analytics.trackUserView(user.id); // 사용자 조회 분석
  }, [user.id]);
  
  return <div className="profile">{user.name}</div>;
}

 

 

🔄 두 단계의 동기화: 완벽한 조화

Render Phase와 Commit Phase는 서로 밀접하게 연결되어 있으며, 효율적인 UI 업데이트를 위해 조화롭게 작동합니다.

📌 단계적 진행

리액트는 다음과 같은 순서로 렌더링을 진행합니다

  1. 상태 변화 감지 🔍
  2. Render Phase 시작 🧠
  3. 변경사항 계산 🧮
  4. (필요시) 다른 높은 우선순위 작업 처리 ⚡
  5. Commit Phase 시작 🎨
  6. DOM 업데이트 🖥️
  7. 사이드 이펙트 실행 🔄

🚦 병목 관리

두 단계의 분리는 성능 최적화에 큰 도움이 됩니다

  • 일관성 유지 ✅: 모든 변경사항이 한 번에 반영됨
  • 불필요한 재렌더링 방지 🛑: 효율적인 업데이트만 수행
  • 우선순위 관리 📊: 중요한 업데이트를 먼저 처리

 

 

🎯 실생활 예시로 이해하기

🛒 쇼핑 카트 시나리오

function ShoppingCart() {
  const [items, setItems] = useState([]);
  
  function addItem(product) {
    // 상태 업데이트 트리거
    setItems([...items, product]);
    // 👆 이 시점에서 render phase 시작
    // render phase에서 가상 DOM 업데이트
    // commit phase에서 실제 DOM에 카트 아이템 추가
  }
  
  return (
    <div className="cart">
      {items.map(item => <CartItem key={item.id} item={item} />)}
      <button onClick={() => addItem({id: Date.now(), name: '상품'})}>
        상품 추가
      </button>
    </div>
  );
}
  1. 사용자가 '상품 추가' 버튼 클릭 👆
  2. Render Phase:
    • 리액트가 새 상품이 추가된 상태로 컴포넌트 다시 계산 🧠
    • 가상 DOM에서 어떤 부분이 변경되었는지 파악 🔍
  3. Commit Phase:
    • 실제 DOM에 새 상품 요소 추가 🎨
    • 브라우저가 새 요소 그리기 🖥️
    • 관련 효과(예: 애니메이션) 실행 ✨

 

 

🚀 React 18과 Concurrent 렌더링

React 18에서는 Concurrent Mode가 도입되어 render phase와 commit phase의 관계가 더욱 흥미롭게 변화했습니다!

⚡ Concurrent Mode의 특징

  • 렌더링 중단 및 재개 🔄: 더 중요한 작업이 있으면 현재 렌더링을 잠시 중단할 수 있습니다
  • 우선순위 기반 업데이트 📊: 사용자 상호작용 같은 중요한 업데이트를 먼저 처리합니다
  • 점진적 렌더링 📈: 큰 목록이나 복잡한 UI를 조금씩 렌더링할 수 있습니다

💡 예시: 타이핑과 검색 결과

사용자가 검색창에 타이핑하는 동안 검색 결과를 표시하는 상황을 생각해보세요:

function SearchBar() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);
  
  // 사용자 타이핑은 높은 우선순위로 처리
  // 검색 결과 렌더링은 낮은 우선순위로 처리될 수 있음
  
  return (
    <div>
      <input 
        value={query} 
        onChange={e => setQuery(e.target.value)} 
        placeholder="검색어 입력..."
      />
      <ResultsList results={results} />
    </div>
  );
}

Concurrent Mode에서는:

  1. 사용자가 타이핑할 때마다 입력 필드 업데이트는 높은 우선순위로 처리 ⚡
  2. 검색 결과 렌더링은 낮은 우선순위로 처리, 필요시 중단 가능 🔄
  3. 사용자가 타이핑을 멈추면 그때 검색 결과를 완전히 렌더링 ✅

이렇게 하면 타이핑이 끊김 없이 부드럽게 동작하며 사용자 경험이 향상됩니다!

📝 마무리

리액트의 render phase와 commit phase는 리액트가 효율적으로 UI를 업데이트하는 핵심 메커니즘입니다:

  • Render Phase 🧠: 무엇을 그릴지 계획하는 단계 (가상 DOM)
  • Commit Phase 🎨: 실제로 화면에 그리는 단계 (실제 DOM)

이 두 단계의 분리는 리액트가 복잡한 UI 업데이트를 효율적으로 관리하고, 사용자에게 부드러운 경험을 제공할 수 있게 해줍니다. React 18의 Concurrent Mode를 통해 이러한 장점은 더욱 강화되었습니다!

리액트의 내부 작동 방식을 이해하면 더 효율적인 컴포넌트를 작성하고 최적화할 수 있습니다. 이 지식이 여러분의 리액트 개발에 도움이 되길 바랍니다! 😊

 


리액트의 render phase와 commit phase에 대해서 설명해주세요.

리액트의 렌더링 과정은 크게 두 가지 단계로 나눌 수 있습니다. render phase commit phase입니다.

먼저 render phase는 리액트가 변화된 상태나 props에 따라 어떤 UI가 변경되어야 할지를 결정하는 단계입니다. 이 과정에서는 실제로 DOM을 업데이트하지 않고, 변경사항을 가상 DOM에서 계산하여 비교합니다. 이 단계는 순수하게 계산과정이기 때문에 성능에 영향을 주지 않도록 중단되거나 다시 실행될 수 있으며, React 18에서 도입된 Concurrent Mode를 통해 비동기적으로 처리될 수도 있습니다.

 

다음으로 commit phase는 실제로 변화된 UI를 DOM에 반영하는 단계입니다. 이때 리액트는 가상 DOM에서 계산된 결과를 실제 DOM에 적용하고, 변화된 UI를 브라우저에 렌더링합니다. DOM 업데이트 이후에는 useEffect와 같은 사이드 이펙트를 발생시키는 훅들이 실행됩니다.

 

요약하면 render phase변화된 UI를 결정하는 계산 과정이고, commit phase는 그 계산된 결과를 실제로 반영하는 단계입니다.

 

그럼 render phase와 commit phase가 동기화될 때의 특징이 있을까요? 🤔

크게 두 가지로 말씀드릴 수 있습니다. 단계적 진행 병목 관리입니다.

첫번째로 render phase가 완료되면 리액트는 즉시 commit phase를 실행하지 않고, 다른 높은 우선순위 작업이 있다면 먼저 처리한 후 나중에 commit phase를 실행할 수 있습니다. 이러한 단계적 진행을 통해 React는 동기화가 필요한 작업을 효율적으로 관리하여 사용자 경험을 개선합니다.

두번째로 병목 관리입니다. render phase에서 모든 변경 사항이 Fiber Tree에 준비된 상태에서 commit phase로 넘어가므로, render와 commit 단계의 일관성이 유지됩니다. 이렇게 두 단계는 순차적으로 작동하여, UI가 정확하게 동기화되고 불필요한 재렌더링을 방지합니다.

728x90

리액트의 Strict Mode에 대해서 설명해주세요.

리액트에서 StrictMode는 주로 개발 중에 발생할 수 있는 잠재적인 문제를 사전에 감지하고 예방하기 위해 사용됩니다.

 

첫째, 오래된 라이프사이클 메서드와 비권장 API의 사용을 감지합니다.

예를 들어, componentWillMount(), componentWillReceiveProps()와 같은 메서드는 더 이상 사용이 권장되지 않는데, StrictMode는 이러한 메서드들이 코드에 포함된 경우 경고를 표시해줍니다. 이를 통해 개발자가 최신 React API를 사용하여 보다 안정적이고 효율적인 코드를 작성하도록 돕습니다.

 

둘째, 의도치 않은 부수 효과를 방지합니다. 리액트는 컴포넌트의 렌더링이 예측 가능하고 순수하게 이루어지기를 기대합니다. StrictMode는 이를 검증하기 위해 useEffect(), useState() 등 일부 훅이나 메서드를 두 번씩 실행합니다. 이렇게 두 번 실행하는 이유는, 동일한 결과가 나오는지 확인함으로써 컴포넌트가 사이드 이펙트를 일으키지 않고 순수하게 동작하는지를 검사하기 위함입니다.

이러한 검증이 중요한 이유는 예기치 않은 동작이나 버그를 사전에 방지하기 위해서입니다. 개발 환경에서 두 번씩 실행해봤을 때 문제가 발생하지 않으면, 프로덕션에서도 안전하게 실행된다는 신호라고 볼 수 있습니다. 이 과정에서 부수 효과가 감지되면 개발자는 코드를 수정해야 할 필요가 있습니다.

이렇듯 StrictMode는 개발자가 더욱 안전하고 효율적인 코드를 작성할 수 있도록 도와주는 도구입니다.

 

코드가 두 번씩 실행되면 성능에 문제가 발생하지 않나요? 🤔

이러한 두 번 실행되는 현상은 개발 모드에서만 발생하고, 실제 프로덕션 빌드에서는 정상적으로 한 번만 실행되기 때문에 성능에 영향을 미치지 않습니다.

728x90

들어가며 🚪

자바스크립트로 개발을 하다 보면 객체나 배열을 복사해야 하는 상황이 자주 발생합니다. 특히 React나 Node.js 애플리케이션에서는 데이터의 불변성(immutability)을 유지하기 위해 원본 데이터를 직접 수정하지 않고 복사본을 만들어 작업하는 것이 중요합니다. 이때 '얕은 복사(Shallow Copy)'와 '깊은 복사(Deep Copy)'의 차이를 이해하는 것이 매우 중요한데요, 이 두 개념을 쉽게 이해해 봅시다! 👨‍💻👩‍💻

 

원시 타입 vs 참조 타입 💡

먼저, 자바스크립트의 데이터 타입을 이해해야 합니다

원시 타입(Primitive Types) 📌

  • String, Number, Boolean, null, undefined, Symbol, BigInt
  • 값 자체가 변수에 저장됨
  • 복사할 때 값이 그대로 새 변수에 복사됨

참조 타입(Reference Types) 🔗

  • Object, Array, Function
  • 메모리 주소(참조)가 변수에 저장됨
  • 복사할 때 주소값만 복사되어 같은 데이터를 가리키게 됨

바로 여기서 얕은 복사와 깊은 복사의 차이가 발생합니다!

 

얕은 복사(Shallow Copy) 란? 🌊

얕은 복사는 객체의 최상위 속성들만 새로운 메모리에 복사하고, 중첩된 객체는 여전히 원본 객체의 참조를 유지하는 복사 방법입니다.

얕은 복사의 특징 🏷️

  • 최상위 속성만 새로운 메모리에 복사됨
  • 중첩된 객체나 배열은 참조만 복사됨
  • 중첩된 객체를 수정하면 원본도 영향을 받음

얕은 복사 방법들 🛠️

1. Object.assign() 사용하기

const original = { name: "철수", scores: [90, 85, 95] };
const copied = Object.assign({}, original);

copied.name = "영희"; // OK: 최상위 속성 변경은 원본에 영향 없음
copied.scores.push(100); // 주의: 중첩 객체 변경은 원본도 변경됨!
console.log(original.scores); // [90, 85, 95, 100] 😱

2. 스프레드 연산자(...) 사용하기

const original = { user: { name: "철수", age: 25 }, active: true };
const copied = { ...original };

copied.active = false; // OK: 최상위 속성만 독립적으로 변경
copied.user.age = 26; // 주의: user 객체는 여전히 참조로 연결됨!
console.log(original.user.age); // 26 😱

3. 배열의 얕은 복사

const originalArray = [1, 2, { id: 1, value: "a" }];
const copiedArray = [...originalArray]; // Array.slice()도 동일하게 작동

copiedArray[0] = 99; // OK: 원시값 변경은 독립적
copiedArray[2].value = "b"; // 주의: 객체 참조는 공유됨!
console.log(originalArray[2].value); // "b" 😱

 

깊은 복사(Deep Copy) 란? 🏊‍♂️

깊은 복사는 객체의 모든 수준의 속성을 새로운 메모리에 완전히 복사하는 방법입니다. 중첩된 객체까지 모두 새로운 참조를 갖게 됩니다.

깊은 복사의 특징 📝

  • 모든 중첩 수준의 객체가 새롭게 생성됨
  • 원본 객체와 완전히 독립적인 사본이 만들어짐
  • 어느 한쪽을 수정해도 다른 쪽에 영향을 주지 않음

 

깊은 복사 방법들 🔧

1. JSON.parse() + JSON.stringify() 사용하기

const original = { name: "철수", scores: [90, 85, 95], info: { age: 20 } };
const deepCopied = JSON.parse(JSON.stringify(original));

deepCopied.scores.push(100); // 안전: 중첩 배열도 독립적
deepCopied.info.age = 21; // 안전: 중첩 객체도 독립적
console.log(original.scores); // [90, 85, 95] ✅
console.log(original.info.age); // 20 ✅

2. lodash의 cloneDeep 사용하기 (라이브러리 사용)

import _ from 'lodash';

const original = { 
  user: { name: "철수", hobbies: ["축구", "게임"] }, 
  created: new Date() 
};
const deepCopied = _.cloneDeep(original);

// 모든 수준의 변경이 안전함!
deepCopied.user.hobbies.push("코딩");
console.log(original.user.hobbies); // ["축구", "게임"] ✅

3. React에서 중첩 객체 복사하기 (함수형 접근)

// React에서 상태 업데이트 시 중첩 객체 안전하게 수정하기
const updateUserHobby = (user) => {
  return {
    ...user,
    hobbies: [...user.hobbies, "코딩"],
    info: { ...user.info, age: user.info.age + 1 }
  };
};

 

JSON 방식의 깊은 복사 주의점 ⚠️

JSON을 이용한 깊은 복사는 간단하지만 몇 가지 제한이 있습니다

  • 함수, Map, Set, Date 객체 등은 제대로 복사되지 않음
  • undefined와 같은 특수값이 손실될 수 있음
  • 순환 참조(Circular Reference)가 있으면 오류 발생

실전 사용 예시: React에서 중첩된 상태 업데이트 🔄

React에서는 상태의 불변성을 유지하는 것이 중요합니다. 다음은 중첩된 객체를 안전하게 업데이트하는 예시입니다:

function ProfileEditor() {
  const [user, setUser] = useState({
    name: "김철수",
    contact: { email: "chulsoo@example.com", phone: "010-1234-5678" },
    tags: ["개발자", "리액트"]
  });

  // 중첩된 속성 업데이트 (깊은 복사 원리 활용)
  const updateEmail = (newEmail) => {
    setUser({
      ...user,                      // 1단계 얕은 복사
      contact: {                    // contact 객체 새로 생성
        ...user.contact,            // 기존 contact 속성 복사
        email: newEmail             // email만 업데이트
      }
    });
  };
}

 

얕은 복사 vs 깊은 복사: 언제 무엇을 사용할까? 🤔

얕은 복사가 적합한 경우

  • 단순한 구조의 객체를 다룰 때
  • 성능이 중요하고 중첩 객체를 수정할 일이 없을 때
  • 의도적으로 참조를 공유하고 싶을 때

깊은 복사가 적합한 경우

  • 복잡한 중첩 구조의 객체를 안전하게 복사할 때
  • 원본 데이터의 불변성을 완벽하게 유지해야 할 때
  • React, Redux 등에서 상태 관리 시

정리 📝

  • 얕은 복사: 최상위 속성만 새로 복사, 중첩 객체는 참조 공유 🌊
  • 깊은 복사: 모든 중첩 수준까지 완전히 새로 복사 🏊‍♂️
  • 주의점: 복사 방법에 따라 사이드 이펙트가 발생할 수 있음 ⚠️

자바스크립트에서 객체와 배열을 다룰 때, 얕은 복사와 깊은 복사의 차이를 이해하는 것은 버그를 예방하고 코드의 예측 가능성을 높이는 데 매우 중요합니다. 특히 React나 Redux와 같은 프레임워크에서는 상태의 불변성을 유지하기 위해 이 개념을 제대로 이해하고 있어야 합니다! 💪

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

리액트의 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

+ Recent posts