26~27일차 React & Redux

정상희·2025년 4월 10일

코딩공부

목록 보기
35/60
post-thumbnail

현재 오즈코딩스쿨 강의를 통해 프론트엔드를 학습하고 있습니다.
본 포스트는 해당 강의에 대한 내용 정리를 목적으로 합니다.

1. React 컴포넌트 생명 주기

React 컴포넌트의 생명 주기(lifecycle)는 컴포넌트가 생성되고, 업데이트되고, 제거될 때까지의 과정을 의미한다. 함수형 컴포넌트와 클래스형 컴포넌트에서 사용하는 방식이 다르며, 요즘은 함수형 컴포넌트 + Hooks가 표준이다.

1) 함수형 컴포넌트에서의 생명 주기 (with useEffect)

함수형 컴포넌트는 생명 주기를 useEffect 훅으로 구현할 수 있다.

import { useEffect } from 'react';

function MyComponent() {
  useEffect(() => {
    // 컴포넌트가 마운트될 때 실행된다.
    console.log('컴포넌트가 처음 나타남');

    return () => {
      // 컴포넌트가 언마운트될 때 실행된다.
      console.log('컴포넌트가 사라짐');
#     };
  }, []); // 빈 배열을 넣으면 최초 1번만 실행된다.

  useEffect(() => {
    // 특정 값이 바뀔 때마다 실행된다.
    console.log('업데이트됨');
  }, [/* 의존성 배열 */]);

  return <div>안녕하세요</div>;
}

2) 클래스형 컴포넌트의 생명 주기 메서드

클래스형 컴포넌트는 생명 주기에 따라 아래 메서드들이 호출된다.

생명주기 단계메서드설명
마운트constructor()컴포넌트가 생성될 때 호출된다.
마운트render()화면에 JSX를 렌더링할 때 호출된다.
마운트componentDidMount()마운트가 완료된 직후에 호출된다.
업데이트shouldComponentUpdate()리렌더링 여부를 결정하는 메서드이다.
업데이트componentDidUpdate()컴포넌트가 업데이트된 후 호출된다.
언마운트componentWillUnmount()컴포넌트가 제거되기 직전에 호출된다.

class MyComponent extends React.Component {
  componentDidMount() {
    console.log('마운트 완료');
  }

  componentDidUpdate(prevProps, prevState) {
    console.log('업데이트 완료');
  }

  componentWillUnmount() {
    console.log('언마운트 완료');
  }

  render() {
    return <div>Hello</div>;
  }
}

3) React 생명 주기 사용하는 경우

a. 서버에서 데이터를 받아올 때

React에서 서버에서 데이터를 받아오는 시점은 생명 주기 중 마운트 이후가 적절하다.
즉, 컴포넌트가 처음 화면에 나타난 직후, 즉 componentDidMount() 시점에 데이터를 받아오면 된다.

import { useEffect, useState } from 'react';

function UserList() {
  const [users, setUsers] = useState([]);

  useEffect(() => {
    // 마운트 이후 1번만 실행됨
    fetch('https://jsonplaceholder.typicode.com/users')
      .then((res) => res.json())
      .then((data) => setUsers(data));
  }, []); // 빈 배열 → 마운트 시점 1회 실행

  return (
    <ul>
      {users.map((user) => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

🔹 서버에서 데이터를 받아오는 타이밍은?
생명 주기 중 "마운트 직후", 즉 componentDidMount() 또는 useEffect(..., []) 시점이다.

🔹 그 이유는?
→ 컴포넌트가 화면에 완전히 준비된 뒤에 비동기 작업을 수행하는 것이 안전하기 때문이다.

b. 이벤트 핸들러를 사용할 때

React에서는 버튼 클릭, 입력값 변경 등 사용자의 상호작용에 반응하는 함수를 이벤트 핸들러라고 한다.
onClick, onChange, onSubmit 같은 속성에 함수를 연결해서 사용한다.

이벤트 핸들러의 생명 주기상 위치

  • 컴포넌트가 렌더링된 후에 사용자와의 상호작용이 발생하면 실행된다.
  • 즉, 생명 주기 중에서는 "렌더링 이후", 그리고 사용자가 무언가 행동했을 때 실행되는 동적인 로직이다.
import { useState } from 'react';

function ClickButton() {
  const [count, setCount] = useState(0);

  const handleClick = () => {
    setCount(count + 1); // 클릭할 때 실행됨
  };

  return <button onClick={handleClick}>클릭: {count}</button>;
}
  • onClick={handleClick} 처럼 JSX에서 이벤트 속성에 함수 연결
  • 실제 실행은 렌더링 이후 사용자가 클릭할 때 발생함

c. 타이머 함수를 사용할 때

setTimeout이나 setInterval비동기적으로 실행되는 함수다.
컴포넌트가 화면에 렌더링된 뒤에 동작해야 하고, 컴포넌트가 사라질 때에는 반드시 정리(clean-up) 해줘야 한다.

따라서 타이머 함수는 useEffect에서 등록하고, 정리 함수(return)에서 clear 해줘야 한다.

import { useEffect, useState } from 'react';

function Timer() {
  const [seconds, setSeconds] = useState(0);

  useEffect(() => {
    const intervalId = setInterval(() => {
      setSeconds((prev) => prev + 1); // 1초마다 1씩 증가
    }, 1000);

    return () => {
      clearInterval(intervalId); // 언마운트 시 정리
    };
  }, []); // 마운트 시 1회만 실행

  return <div>경과 시간: {seconds}</div>;
}
  • setInterval은 계속 실행되므로 컴포넌트가 사라질 때 제거하지 않으면 메모리 누수가 생긴다.
  • 생명 주기상 useEffectcomponentDidMountcomponentWillUnmount 역할을 같이 할 수 있기 때문에 딱 맞다.

4) React 생명 주기 함수의 필요성

a. 초기 설정을 할 수 있기 때문이다.

컴포넌트가 화면에 처음 나타날 때, 데이터 요청(API 호출), 이벤트 등록, 외부 라이브러리 초기화 같은 작업을 해야 할 때가 있다.
이때 생명 주기 함수인 componentDidMount() 혹은 useEffect(..., [])를 사용하면 된다.

👉 예: 서버에서 데이터를 가져와 초기 렌더링에 반영해야 할 때 필요하다.

b. 변경 사항에 반응하기 위함이다.

propsstate가 변경되었을 때 어떤 작업이 필요하다면, 해당 시점을 잡아낼 수 있어야 한다.
이때 componentDidUpdate() 또는 useEffect(..., [의존성])이 사용된다.

👉 예: 특정 state가 바뀌면 로컬 스토리지에 저장하는 등의 작업이 이에 해당한다.

c. 리소스를 정리하기 위함이다.

컴포넌트가 화면에서 사라질 때(언마운트 시), 타이머 제거, 이벤트 해제, 소켓 연결 종료 같은 정리 작업이 필요하다.
이때 componentWillUnmount() 또는 useEffectcleanup 함수가 사용된다.

👉 예: setInterval, setTimeout으로 만든 반복 작업을 제거하지 않으면 메모리 누수가 발생할 수 있다.

d. 성능 최적화를 가능하게 하기 때문이다.

생명 주기 메서드 중 shouldComponentUpdate()는 렌더링을 제어해 불필요한 업데이트를 막을 수 있다.
React.memo와 같은 최적화 방법도 생명 주기 개념을 바탕으로 한다.

👉 예: 변화가 없는 컴포넌트를 반복해서 렌더링하지 않게 하여 앱을 빠르게 유지할 수 있다.



2. React Hooks

React 16.8부터 도입된 기능으로, 기존에는 클래스형 컴포넌트에서만 가능하던 상태 관리(state), 라이프사이클 관리 등을 함수형 컴포넌트에서도 가능하게 만든 것이 Hooks이다.

1) useEffect

useEffect는 함수형 컴포넌트에서 생명 주기(lifecycle)와 관련된 작업을 처리할 수 있게 해주는 Hook이다.
클래스형 컴포넌트의 componentDidMount, componentDidUpdate, componentWillUnmount를 모두 대체할 수 있다.

import { useEffect } from 'react';

useEffect(() => {
  // 실행할 코드

  return () => {
    // 정리(clean-up)할 코드 (선택적)
  };
}, [의존성]);
}

a. 상황별 작동 방식

형태언제 실행되는가설명
useEffect(() => { ... })모든 렌더링마다 실행된다.의존성 배열이 없으면 매번 실행된다.
useEffect(() => { ... }, [])처음 마운트될 때만 실행된다.컴포넌트가 처음 나타날 때 1회 실행된다.
useEffect(() => { ... }, [값])해당 값이 변경될 때만 실행된다.특정 상태나 props가 바뀔 때만 실행된다.(리렌더링)
return () => { ... }언마운트 시 또는 다음 실행 전에 실행된다.타이머 제거, 이벤트 제거 등에 사용된다.(컴포넌트가 화면에서 사라질 때)

의존성 배열 (dependency array)

useEffect의 두 번째 인자인 []"이 useEffect를 언제 실행할지 결정하는 기준"이다.
이 배열 안에 있는 값이 바뀔 때마다 useEffect 내부 코드가 다시 실행된다.

useEffect(() => {
  // 실행할 코드
}, [의존성1, 의존성2, ...]);

React는 렌더링이 반복되기 때문에, 불필요한 실행을 막고 필요한 타이밍에만 실행되도록 의존성 배열을 사용한다.

⚠️ 주의할 점

  • 의존성 배열에 사용 중인 state나 props는 반드시 포함시켜야 한다.
  • 그렇지 않으면 최신 상태를 보장하지 못하고 버그나 경고가 발생할 수 있다.
    (ESLint에서 react-hooks/exhaustive-deps 경고가 뜨는 이유다.)

b. 예제

마운트 시 API 호출

useEffect(() => {
  fetchData(); // 컴포넌트가 처음 나타날 때만 실행됨
}, []);

특정 값 변경 시 반응

useEffect(() => {
  console.log('count가 바뀜:', count);
}, [count]);

언마운트 시 정리(clean-up)

useEffect(() => {
  const timer = setInterval(() => {
    console.log('1초마다 실행됨');
  }, 1000);

  return () => {
    clearInterval(timer); // 언마운트되거나 다음 실행 전에 제거됨
  };
}, []);

input 값 변경 추적

import { useState, useEffect } from 'react';

function Example() {
  const [text, setText] = useState('');

  useEffect(() => {
    console.log('텍스트 변경됨:', text);
  }, [text]);

  return <input value={text} onChange={(e) => setText(e.target.value)} />;
}

2) Controlled Component

폼 요소의 값을 React의 state로 관리하는 컴포넌트를 말한다.
즉, 사용자의 입력값이 DOM이 아닌 React의 상태(state)를 통해 제어되는 방식이다.

import { useState } from 'react';

function ControlledInput() {
  const [inputValue, setInputValue] = useState('');

  const handleChange = (e) => {
    setInputValue(e.target.value);
  };

  return (
    <div>
      <input value={inputValue} onChange={handleChange} />
      <p>입력한 값: {inputValue}</p>
    </div>
  );
}

이 코드에서 input의 값은 inputValue라는 state가 제어하고 있으며, 입력값을 변경하려면 반드시 setInputValue를 통해야만 한다. 이처럼 값의 흐름이 React 중심으로 통제되는 것이 Controlled 방식이다.

a. 특징

  • value 속성으로 state와 연결되어 있다.

  • onChange 이벤트로 값이 바뀔 때 state를 업데이트한다.

  • React가 입력값을 완전히 통제하기 때문에 상태 추적 및 검증이 용이하다.

  • 입력값 변경 시마다 컴포넌트가 리렌더링된다는 단점도 존재한다.


b. Controlled 방식을 사용하는 이유

  • 입력값 실시간 검증, 조건부 렌더링, 폼 제출 시 유효성 검사 등을 처리하기에 유리하다.
  • 대부분의 React 프로젝트에서 Controlled 방식이 기본으로 사용된다.

c. Uncontrolled Component와 비교

항목Controlled ComponentUncontrolled Component
값 저장 위치React stateDOM 내부
초기 값 설정value + useStatedefaultValue
값 가져오기state로 바로 접근ref.current.value 이용
장점상태 추적 및 유효성 검사에 유리하다구현이 간단하고 빠르다
단점코드 복잡도 및 리렌더 비용이 증가한다값 추적이 어렵고 React와의 연결성이 낮다

3) useRef

useRef는 React에서 값을 저장하거나, DOM 요소에 직접 접근할 때 사용하는 Hook이다.
렌더링 사이에도 값을 유지할 수 있고, 값이 바뀌어도 컴포넌트를 리렌더링하지 않는다.

a. DOM 요소 접근하기

import { useRef, useEffect } from 'react';

function FocusInput() {
  const inputRef = useRef(null);

  useEffect(() => {
    // 컴포넌트가 마운트되었을 때 input에 포커스
    inputRef.current.focus();
  }, []);

  return <input ref={inputRef} type="text" />;
}
  • useRef(null)로 초기화
  • ref={inputRef}로 DOM 요소에 연결
  • inputRef.current로 해당 DOM에 접근 가능

b. 값 저장용 (변수처럼 사용)

import { useRef } from 'react';

function TimerComponent() {
  const count = useRef(0);

  const handleClick = () => {
    count.current += 1;
    console.log('Clicked:', count.current);
  };

  return <button onClick={handleClick}>Click me</button>;
}
  • count.current에 값을 저장 → 값은 바뀌어도 컴포넌트는 리렌더링되지 않음
  • 주로 타이머 ID 저장, 이전 값 기억 등에서 사용

4) Custom Hook

React의 Hook들을 조합해서 만드는 재사용 가능한 함수를 말한다.
useState, useEffect, useRef 등 기본 훅을 이용해 로직을 캡슐화하고 재사용하기 위한 방식이다. 함수 이름은 반드시 use로 시작해야 하며, 일반 함수처럼 호출해서 내부 로직을 사용할 수 있다.

Custom Hook은 반복되는 로직을 함수처럼 만들어 재사용하기 위한 React의 규칙과 패턴이라 할 수 있다.

a. 기본 구조

function useCustomHook() {
  // useState, useEffect 등 사용 가능
  const [value, setValue] = useState(0);

  useEffect(() => {
    console.log('훅 내부 로직 실행');
  }, []);

  return [value, setValue];
}

이렇게 만든 훅은 일반 컴포넌트에서 다음과 같이 사용할 수 있다.

const [count, setCount] = useCustomHook();

b. 예시

import { useState, useEffect } from 'react';

function useWindowSize() {
  const [size, setSize] = useState({
    width: window.innerWidth,
    height: window.innerHeight,
  });

  useEffect(() => {
    const handleResize = () => {
      setSize({
        width: window.innerWidth,
        height: window.innerHeight,
      });
    };

    window.addEventListener('resize', handleResize);
    return () => window.removeEventListener('resize', handleResize);
  }, []);

  return size;
}

function MyComponent() {
  const { width, height } = useWindowSize();

  return <p>현재 창 크기: {width} x {height}</p>;
}

c. Custom Hook의 장점

  • 로직 재사용이 쉬워진다.

  • 컴포넌트를 더 깔끔하고 읽기 쉽게 만든다.

  • 복잡한 로직을 분리하여 관리하기 좋다.

  • 관심사 분리가 가능하다.


d. Custom Hook 작성 시 주의사항

  • 반드시 use로 시작해야 React가 훅으로 인식한다.

  • 훅 내부에서도 React의 규칙(훅은 최상위에서만 호출해야 함 등)을 지켜야 한다.

  • 일반적으로 훅은 상태나 side-effect를 포함하고 있어야 유용하다.



끝맺음.

음...주말에 예제, 복습 둘다 해야겠다..ㅠㅠ

profile
UI/UX디자이너의 코딩 공부

0개의 댓글