Skip to main content
입문React 공통 모델 · 5

React Effect의 역할: 채팅 연결을 렌더링에서 분리하는 이유

채팅 연결, 메시지 전송과 남은 글자 계산을 한 컴포넌트에서 구분하며 React Effect가 필요한 경계를 판단한다.

마지막 검증 재검증 정책: 제품 버전 의존: 새 주요 버전마다 재검증
목차
표준·구현·측정·해석 표시는 무엇인가요?
  • 표준웹 표준이나 언어 명세가 정한 동작
  • 구현특정 기술이나 브라우저가 실제로 구현한 동작
  • 측정명시한 환경에서 직접 실행해 관찰한 결과
  • 해석앞선 근거에서 도출한 설계 판단
  • 미확인아직 공식 근거나 재현 결과를 확인하지 못한 내용

30초 요약

React 컴포넌트에는 서로 다른 세 종류의 코드가 있다.

  • props와 상태로 UI 값을 구하는 렌더링 계산
  • 버튼 클릭처럼 특정 사용자 행동 때문에 실행하는 사건 처리 함수
  • 화면에 반영된 상태와 React 밖 시스템을 맞추는 Effect

Effect는 “렌더링 뒤에 실행하고 싶은 코드”를 모아 두는 곳이 아니다. 외부 시스템이 없다면 먼저 렌더링 중 계산하거나 사용자 사건에서 실행할 수 있는지 확인한다.

이 글을 관통하는 상황: 메시지를 입력할 때마다 다시 연결된다

다음 예제는 채팅방을 선택하고 메시지 초안을 입력한다. createConnection은 실제 서버 대신 연결과 해제를 콘솔에 기록해 외부 동작을 관찰하게 하는 작은 대역이다. 대역은 실제 시스템과 같은 계약을 가진 단순한 구현을 뜻한다.

import { useState } from 'react'
 
function createConnection(roomId) {
  return {
    connect() {
      console.log(`${roomId} 연결`)
    },
    disconnect() {
      console.log(`${roomId} 해제`)
    },
  }
}
 
function sendMessage(roomId, message) {
  console.log(`${roomId} 전송: ${message}`)
}
 
export default function ChatRoom() {
  const [roomId, setRoomId] = useState('general')
  const [draft, setDraft] = useState('')
 
  const connection = createConnection(roomId)
  connection.connect()
 
  const remaining = 100 - draft.length
 
  function handleSend() {
    sendMessage(roomId, draft)
    setDraft('')
  }
 
  return (
    <section>
      <select value={roomId} onChange={event => setRoomId(event.target.value)}>
        <option value="general">일반</option>
        <option value="support">지원</option>
      </select>
      <input
        value={draft}
        onChange={event => setDraft(event.target.value)}
        aria-label="메시지"
      />
      <p>남은 글자: {remaining}</p>
      <button type="button" onClick={handleSend} disabled={!draft}>
        전송
      </button>
    </section>
  )
}

화면을 처음 열면 general 연결이 기록된다. 문제는 메시지 입력 한 글자마다 draft 상태가 바뀌어 컴포넌트가 다시 Render되고, connection.connect()도 다시 실행된다는 점이다. React는 Render 코드를 여러 번 실행할 수 있으므로 정확한 로그 개수보다 입력만 했는데 연결 동작이 다시 일어났는지를 본다.

잘못된 판단은 “채팅 화면에 연결이 필요하므로 컴포넌트 본문에서 연결한다”는 것이다. 화면 모양을 계산하는 실행과 외부 연결의 수명을 같은 곳에 놓아, 관련 없는 입력까지 연결의 원인으로 만들었다.

Render에서 연결하면 입력도 재연결의 원인이 된다

Render는 사용자 사건과 일대일 대응하지 않는다. 상태가 바뀌거나 부모가 다시 Render되거나 React가 계산을 다시 시도할 때 컴포넌트 본문이 실행될 수 있다. 그래서 본문의 connect()는 “이 화면이 필요로 하는 연결”이 아니라 “이 함수가 실행된 횟수만큼의 연결”이 된다.

Effect는 반영된 상태와 외부 시스템을 맞춘다

위 예제에서는 Render 본문의 두 연결 줄을 지우고 다음처럼 ChatRoom 안에 Effect를 둔다. createConnectionsendMessage는 앞의 구현을 그대로 사용한다.

import { useEffect, useState } from 'react'
 
export default function ChatRoom() {
  const [roomId, setRoomId] = useState('general')
  const [draft, setDraft] = useState('')
 
  useEffect(() => {
    const connection = createConnection(roomId)
    connection.connect()
 
    return () => connection.disconnect()
  }, [roomId])
 
  const remaining = 100 - draft.length
 
  function handleSend() {
    sendMessage(roomId, draft)
    setDraft('')
  }
 
  return (
    <section>
      <select value={roomId} onChange={event => setRoomId(event.target.value)}>
        <option value="general">일반</option>
        <option value="support">지원</option>
      </select>
      <input
        value={draft}
        onChange={event => setDraft(event.target.value)}
        aria-label="메시지"
      />
      <p>남은 글자: {remaining}</p>
      <button type="button" onClick={handleSend} disabled={!draft}>
        전송
      </button>
    </section>
  )
}

Effect에서 반환한 함수는 기존 연결이 더 이상 필요 없을 때 해제하는 정리 함수다. 정리 시점과 개발 환경의 추가 실행은 다음 글에서 집중해서 다룬다.

이제 메시지를 입력해도 연결은 다시 만들어지지 않는다. 채팅방을 general에서 support로 바꾸면 기존 general 연결을 해제한 뒤 support에 연결한다. Effect는 “한 번 실행”을 보장하는 장치가 아니라, 화면에 반영된 roomId와 외부 연결이 계속 일치하도록 만드는 동기화 규칙이다.

계산, 사건, Effect를 원인으로 구분한다

같은 채팅 컴포넌트에서도 코드가 놓일 곳은 원인에 따라 달라진다.

필요한 일직접 원인둘 곳
100 - draft.length로 남은 글자 계산현재 draft로 UI를 계산Render 본문
전송 요청사용자가 전송 버튼을 클릭handleSend 사건 처리 함수
선택한 채팅방 연결 유지그 채팅방이 화면에 반영됨roomId를 읽는 Effect

remaining을 별도 상태에 저장하고 Effect에서 갱신할 필요가 없다. handleSend도 전송 버튼 클릭이 명확한 원인이므로 Effect로 옮기지 않는다.

의존성은 Effect가 읽는 반응형 값에서 결정된다

연결 Effect가 읽는 반응형 값은 roomId다. draft는 Effect 안에서 읽지 않고 메시지 입력은 채팅방 연결을 바꿔야 하는 이유도 아니므로 의존성이 아니다.

의존성 배열은 “적게 실행하려고 원하는 값을 빼는 목록”이 아니다. React Hook의 의존성 누락을 찾는 자동 코드 검사인 린트가 경고하면, 검사를 끄기보다 Effect 안에서 그 값을 읽어야 하는지, Effect 자체가 필요한지를 다시 본다.

React 규칙은 같아도 외부 대상은 플랫폼마다 다르다

Web에서는 브라우저 미디어 API, DOM 기반 외부 위젯, 네트워크 연결이 대상이 될 수 있다. React Native에서는 앱의 전경·배경 변화를 알리는 AppState 같은 플랫폼 API나 네이티브 이벤트 구독이 대상이 될 수 있다. “Effect니까 같은 방식으로 해제된다”고 가정하지 말고 사용하는 Web·Native API의 공식 생명주기 계약을 따로 확인한다.

입력과 방 변경을 따로 검증한다

교정한 컴포넌트에서는 다음 순서로 원인과 동작을 맞춰 본다.

  1. 처음 화면을 열어 현재 방 연결이 기록되는지 확인한다.
  2. 메시지를 입력해 남은 글자 수는 바뀌지만 새 연결은 생기지 않는지 확인한다.
  3. 전송 버튼을 눌러 전송은 한 번 기록되고 입력이 비워지는지 확인한다.
  4. 채팅방을 바꿔 이전 방 해제 뒤 새 방 연결이 기록되는지 확인한다.
문제       메시지 입력만 해도 채팅 연결이 다시 생김
잘못된 판단 화면에 연결이 필요하므로 컴포넌트 본문에서 connect 호출
관찰       draft 변경으로 Render될 때마다 connect 로그가 추가됨
원인       순수한 UI 계산과 외부 연결 수명을 같은 실행에 둠
행동       roomId를 읽는 Effect에서 연결하고 기존 연결을 정리
검증       입력은 계산만, 전송은 클릭 때만, 방 변경은 해제 후 새 연결

풀 리퀘스트에서는 다음 순서로 확인한다.

  • 이 코드는 props와 상태만으로 값을 계산하는가? 그렇다면 Render에 둔다.
  • 특정 클릭·제출·입력이 직접 원인인가? 그렇다면 해당 사건 처리 함수에 둔다.
  • 화면에 반영된 상태와 React 밖 시스템을 계속 맞춰야 하는가? 그렇다면 Effect 경계를 검토한다.
  • Effect가 읽는 반응형 값을 의존성에서 임의로 빼지 않았는가?
  • Web이나 Native의 실제 외부 API가 요구하는 해제 계약을 확인했는가?

기억할 질문은 하나다.

계산인가, 사용자 사건인가, 외부 시스템 동기화인가?

다음 글에서는 Effect가 다시 실행되거나 컴포넌트가 사라질 때 타이머, 이벤트 listener, 구독과 요청을 언제 어떻게 정리해야 하는지 살펴본다.

Active recall

기억에서 꺼내 보기

답을 쓰지 않아도 됩니다. 먼저 머릿속으로 답한 뒤 펼쳐서 정답·이유·흔한 오해를 비교하세요.

  1. 01채팅방 컴포넌트 본문에서 매번 `connection.connect()`를 호출하면 왜 입력할 때도 연결이 늘어날 수 있는가?

    정답

    입력 상태가 바뀔 때마다 Render가 다시 실행되고, 연결 호출도 순수한 UI 계산의 일부처럼 다시 실행되기 때문이다.

    왜 그런가

    채팅방이 화면에 반영된 동안 필요한 연결은 Render 본문이 아니라 roomId를 의존성으로 둔 Effect가 맡아야 한다.

    관련 설명 다시 읽기
    지금 어느 정도 기억났나요?

    선택한 상태와 다음 복습일은 이 브라우저에만 저장됩니다.

  2. 02남은 글자 수 계산, 메시지 전송, 채팅 연결은 각각 어디에 두어야 하는가?

    정답

    남은 글자 수는 Render에서 계산하고, 메시지 전송은 전송 버튼의 사건 처리 함수에서 실행하며, 화면에 보이는 채팅방 연결은 Effect에서 동기화한다.

    왜 그런가

    계산에 외부 변화가 필요하지 않은지, 특정 사용자 사건이 원인인지, 렌더링된 상태와 외부 시스템을 맞춰야 하는지 순서대로 구분한다.

    관련 설명 다시 읽기
    지금 어느 정도 기억났나요?

    선택한 상태와 다음 복습일은 이 브라우저에만 저장됩니다.

  3. 03채팅 연결 Effect의 의존성 배열에 `draft`가 아니라 `roomId`가 들어가는 이유는 무엇인가?

    정답

    연결 설정이 읽는 반응형 값은 roomId이며, draft 변경은 외부 연결을 다시 맞춰야 하는 이유가 아니기 때문이다.

    왜 그런가

    의존성은 실행 횟수를 임의로 조절하는 목록이 아니라 Effect 코드가 읽는 props와 상태에서 결정된다.

    관련 설명 다시 읽기
    지금 어느 정도 기억났나요?

    선택한 상태와 다음 복습일은 이 브라우저에만 저장됩니다.

  4. 04React DOM과 React Native에서 Effect를 판단하는 질문은 같고 실제 외부 대상은 어떻게 달라지는가?

    정답

    React가 관리하지 않는 시스템과 동기화하는지 묻는 판단은 같지만, Web에서는 브라우저 API나 DOM 위젯, Native에서는 플랫폼 API나 네이티브 구독이 대상이 될 수 있다.

    왜 그런가

    Effect는 공통 React 모델이고 연결하는 실제 시스템의 생명주기와 해제 규칙은 각 플랫폼 문서에서 확인한다.

    관련 설명 다시 읽기
    지금 어느 정도 기억났나요?

    선택한 상태와 다음 복습일은 이 브라우저에만 저장됩니다.

출처와 검증 범위

아래 날짜는 링크를 마지막으로 열어 본 날입니다. 문서 상단의 검증일은 글의 설명과 적용 범위를 다시 확인한 날입니다.

  1. Synchronizing with EffectsReact · 공식 문서 · 확인 2026-08-08
  2. useEffectReact · 공식 문서 · 확인 2026-08-08
  3. You Might Not Need an EffectReact · 공식 문서 · 확인 2026-08-08
  4. Components and Hooks must be pureReact · 공식 문서 · 확인 2026-08-08
  5. AppStateReact Native · 공식 문서 · 확인 2026-08-08
이 문서의 마지막까지 읽었습니다.