React - docs, 규칙

Hunter Joe·2025년 4월 17일

React 들어가기

  • 본 내용은 모두 React 공식문서 19.1을 바탕으로 작성되었음을 미리 알립니다.
  • 모든 내용을 다루지 않았습니다. 제가 생각하기에 중요한 내용이라 생각되는 내용을 바탕으로 작성되었습니다.
  • 모든 용어는 원문(영어)를 기반으로 다룰 것입니다.

내가 React 공식문서를 뜯어보는 이유
1. useState, useEffect 등 기초적인 상태관리만 할 줄 알지 그 외의 것은 알지 못한다.
2. 아름다운 코드, 효율적인 코드를 작성하기 위해
3. 재밌으니깐

React의 규칙

React는 아래 3가지 방식을 따라서 개발하기를 권장하고 있다.
1. Components and Hooks must be pure
2. React calls Components and Hooks
3. Rules of Hooks

Components and Hooks must be pure

순수 함수

  • 호출되기 전에 존재했던 객체나 변수는 변경하지 않는다.
    → 외부 상태를 변경하지 않는 함수
  • 동일한 input에 대해 동일한 output을 반환한다.

순수성을 보장하기에 재사용이 가능하고 이를 캐싱할 수 있다.
useMemo, Memo, useCallback와 같은 최적화 hook들을 쓸 수 있는 이유

순수성이 중요한 이유?


1). Idempotent(멱등성)
→ 수학이나 컴퓨터 과학에서 연산을 여러 번 반복해도 결과가 달라지지 않는 성질

2). Has no side Effects in render
→ 랜더링 중에는 Side Effect를 발생시킬 수 있는 행동을 하지 말라는 것
Side Effect를 발생시킬 수 있는 행동은 렌더링 이후에 진행해라.
useEffect와 이벤트 핸들러는 렌더링 이후에 실행해야 하는 이유이기도 하다.

Side Effect?
Side Effect는 절대 나쁜 것이 아님 의도되지 않은 Side Effect가 우리를 힘들게 한다.

let str = "Hello Universe"
otherStr= str
// 위 코드는 str과 otherStr에 문자열이 할당된 것이라 
// 총 2번의 side effect가 나타났다고 볼 수 있다.

3). Does not mutate noen-local values
→ "컴포넌트 안에서 만든 게 아니라면, 렌더링 중에 그 값을 바꾸면 말라"
렌더링 중에는 외부 값을 변경해서는 안된다는 것

When is it okay to have mutation?

변경은 언제 일어나도 괜찮을까?
공식 문서에 작성된 예시 코드를 기반으로 살펴보자

function FriendList({ friends }) {
  const items = []; // ✅ Good: locally created -> 지역
  for (let i = 0; i < friends.length; i++) {
    const friend = friends[i];
    items.push(
      <Friend key={friend.id} friend={friend} />
    ); // ✅ Good: local mutation is okay 
  }
  return <section>{items}</section>;
}

위 코드에서 push메서드는 local items = []를 mutate하고 있기 때문에 괜찮음
렌더링 시에 전혀 지장이 없다.
반면 아래 코드는 리액트에서 문제점이라고 지적하는 side Effect를 발생시킨다.

const items = []; // 🔴 Bad: created outside of the component
function FriendList({ friends }) {
  for (let i = 0; i < friends.length; i++) {
    const friend = friends[i];
    items.push(
      <Friend key={friend.id} friend={friend} />
    ); // 🔴 Bad: mutates a value created outside of render
  }
  return <section>{items}</section>;
}

<FriendList /> 컴포넌트가 다시 실행될 때마다 우리는 itemsfriends들을 계속해서 추가하게 됨
→ 그 결과로 친구 목록이 중복되어 렌더링 되는 Side Effect가 생김
이것은 렌더링 중 관측 가능한 부작용(observable side effects)를 발생시키기 때문에 React의 규칙을 위반하게 되는 것이다.

Lazy initialization

function ExpenseForm() {
  SuperCalculator.initializeIfNotReady(); // ✅ Good: if it doesn't affect other components
  // Continue rendering...
}

SuperCalculator.initializeIfNotReady()는 외부 상태나 UI에 직접적인 영향을 주지 않고
컴포넌트 내부에서 은밀히 동작하는 초기화 함수로(캡슐화)
관찰 가능한 부작용(observable side effect)이 아니기 때문에 안전하다.

Changing the DOM

function ProductDetailPage({ product }) {
  document.title = product.title; // 🔴 Bad: Changes the DOM
}

사용자에게 직접 보이는 변화를 일으키는 side effect은, React 컴포넌트의 렌더링 로직 안에서 허용되지 않는다.
즉, 단순히 컴포넌트 함수를 호출하는 것만으로 화면에 변화가 생기면 안 된다.

Props and state are immutable

props와 state 값은 렌더링 후 업데이트되는 스냅샷이라고 생각하면 됩니다. 따라서 props나 state 변수를 직접 수정하지 않고, 새 props를 전달하거나 제공된 setter 함수를 사용하여 React에 다음 컴포넌트 렌더링 시 state를 업데이트해야 한다고 알립니다.

React를 써본 개발자라면 상태 변경은 setter를 통한 변경에 대해 알 것이라고 생각하기에 이 부분에 대해서는 넘어가겠음

여기까지가 1. Components and Hooks must be pure해야 된다는 첫 번째 규칙에 대해 알아본 것이다.

React calls Components and Hooks

React는 사용자 경험을 최적화하기 위해 컴포넌트와 훅(Hook)을 필요할 때 렌더링하는 역할을 한다.
React는 선언형(declarative)이다.
즉,"무엇을 렌더링할지"만 컴포넌트 안에서 React에게 말하면 되고,
어떻게 그걸 화면에 보여줄지는 React가 알아서 처리한다.

  • 선언형(declarative)
return <button disabled={isLoading}>Submit</button>;
// → "isLoading이면 버튼을 비활성화해줘"
// → React가 알아서 DOM 업데이트, 이벤트 연결 등을 처리해 줌
  • 명령형 (Imperative)
const button = document.querySelector("button");
if (isLoading) {
  button.setAttribute("disabled", true);
} else {
  button.removeAttribute("disabled");
}
// → 직접 DOM에 명령해서 처리함 → 유지보수 어려움

Never call component functions directly

절대 컴포넌트 함수를 직접 호출하지 말 것
React는 렌더링 과정에서 어떤 컴포넌트를 언제 호출할지 스스로 판단해야 하며
우리는 JSX를 사용해 React에게 "무엇을 렌더링할지"를 선언적으로 알려주기만 하면 된다.

function BlogPost() {
  return <Layout><Article /></Layout>; // ✅ Good: Only use components in JSX
}
function BlogPost() {
  return <Layout>{Article()}</Layout>; // 🔴 Bad: Never call them directly
}

리액트 스스로 렌더링을 조정하도록 하면 다음과 같은 이점을 얻을 수 있다.

- Components become more than functions.
→ React는 트리 구조에서 각 컴포넌트를 고유하게 인식하고 있기 때문에
그 컴포넌트에 useState, useEffect 같은 Hook 기능을 연결해서 확장할 수 있다

- Component types participate in reconciliation
→ JSX로 처럼 컴포넌트를 "React가 호출하도록" 작성하면
React는 이 컴포넌트를 트리의 고유한 단위로 인식하고
전환 시 올바르게 unmount/mount 해준다.

- React can enhance your user experience
→ 예를 들어, React가 컴포넌트를 호출하는 방식을 통해
브라우저가 컴포넌트 간에 다른 작업을 처리할 수 있도록 시간을 줄 수 있다.
이 덕분에 큰 컴포넌트 트리를 리렌더링할 때 메인 스레드를 블로킹하지 않고 부드럽게 처리할 수 있게 된다.

- A better debugging story
→ 컴포넌트가 라이브러리(React)가 인식할 수 있는 일급 시민(First-Class Citizens, 일급 객체)이라면,
개발 중에 내부 상태나 구조를 살펴볼 수 있는 강력한 개발자 도구(devtools)를 만들 수 있다.

- More efficient reconciliation
→ React는 트리 구조에서 어떤 컴포넌트를 다시 렌더링해야 하는지 정확히 판단할 수 있으며 변경되지 않은 컴포넌트는 건너뛸 수 있다. (앱이 더 빠르고 반응성이 좋아짐)

Never pass around Hooks as regular values

Hook을 일반 값처럼 전달하면 안된다고 한다.
Hook은 오직 컴포넌트 함수나 다른 Hook 내부에서만 호출되어야 한다.
Hook은 컴포넌트에 React의 기능(예: 상태 관리, 이펙트 등)을 추가하는 도구다.
따라서 항상 직접 호출되는 함수 형태로 사용해야 하며 값처럼 전달하거나 외부로 빼서 넘기면 안됨

이 규칙을 지켜야 해당 컴포넌트가 개발자가 보기에도 직관적으로 무슨 일을 하는지 추론할 수 있다.

const counter = useState(0);        // ❌ 컴포넌트 바깥에서 호출
<Component someState={counter} />   // ❌ 훅 결과를 그냥 값처럼 넘김

Don’t dynamically mutate a Hook

  • 훅을 동적으로 변경하지 말 것
  • 훅은 가능한 정적으로 유지해야 한다. (Hooks should be as “static” as possible.)
import { useState } from "react";

// ❌ 훅을 감싸는 "Higher-Order Hook" 함수
function withLogging(hook) {
  return function () {
    console.log("Hook 호출됨!");
    return hook(); // ❌ 여기서 훅이 감싸져서 호출됨
  };
}

function Counter() {
  const useLoggedState = withLogging(() => useState(0)); // ❌ 훅을 감싼 구조
  const [count, setCount] = useLoggedState(); // ❌ 훅이 아니라고 React는 판단함

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

Don’t dynamically use Hooks

훅을 동적으로 사용하지 말 것
예를 들어, Hook을 값처럼 컴포넌트에 주입(Dependency Injection)하는 방식으로 사용하지 말 것
→ 어떤 컴포넌트가 사용할 Hook을 props로 받아서 실행하는 식의 코드로 쓰지 말라는 것

function ChatInput() {
  return <Button useData={useDataWithLogging} /> // 🔴 Bad: don't pass Hooks as props
}

훅을 호출할 때 해당 컴포넌트에 inline으로 구현하고 컴포넌트 안에서 로직을 처리 할 것

function ChatInput() {
  return <Button />
}

function Button() {
  const data = useDataWithLogging(); // ✅ Good: Use the Hook directly
}

function useDataWithLogging() {
  // If there's any conditional logic to change the Hook's behavior, it should be inlined into
  // the Hook
}

훅을 동적으로 사용하면 app의 복잡도가 증가하고
local reasoning(코드를 보고 직관적으로 이해할 수 있는 능력)이 떨어짐
→ 생산상 저하

또한 훅은 조건부로 호출되면 안된다는 규칙이 있는데 동적 사용은 이런 규칙을 무심코 깨기 쉬운 구조로 만든다.

만약 테스트 중에 특정 컴포넌트를 mocking할 필요가 생긴다면 차라리 서버 응답을 mock 처리(canned data)하는 것이 낫다.
가능하다면, 엔드 투 엔드 테스트(E2E)로 앱을 테스트하는 것이 더 효과적일 때가 많다.

TESTING TIP

  • 테스트 중 특정 컴포넌트를 mocking 할 필요가 생긴다면 차라리 서버 응답을 mock 처리하는게 낫다.
  • 가능하다면 E2E로 앱을 테스트 하는 것이 더 효과적일 때가 많다.

여기까지가 2. React calls Components and Hooks이라는 두 번째 규칙에 대해 알아본 것이다.

Rules of Hooks

모든 훅은 아래의 규칙을 따를 것
- Only call Hooks at the top level(최상위 호출)
- Only call Hooks from React functions(리액트 함수에서만 호출할 것)

반복문, 조건문, 중첩 함수, try/catch/finally 블록안에서 호출하지 말 것
항상 훅은 React 함수 컴포넌트의 최상단에서 호출해야 합니다.
조건부로 일찍 리턴하는 구문이 있다면, 그보다 먼저 훅을 호출해야 한다.
훅은 오직 React가 함수 컴포넌트를 렌더링하는 동안에만 호출할 수 있다.

function MyComponent({ isLoggedIn }) {
  if (!isLoggedIn) return null; // ❌ 훅 호출 전에 리턴해버림

  const [user, setUser] = useState(null); // ❌ 이 줄은 아예 실행되지 않음

  return <p>{user}</p>;
}

🔴 Do not call Hooks inside conditions or loops.
🔴 Do not call Hooks after a conditional return statement.
🔴 Do not call Hooks in event handlers.
🔴 Do not call Hooks in class components.
🔴 Do not call Hooks inside functions passed to useMemo, useReducer, or useEffect.
🔴 Do not call Hooks inside try/catch/finally blocks.
If you break these rules, you might see this error.

NOTE
위 규칙은 eslint-plugin-react-hooks을 통해 막을 수 있다.

여기까지 리액트가 권장하는 규칙들을 자세하게 알아본셈이다.
리액트의 규칙만 지키더라도 훌륭한 코드를 그리고 각 훅이 왜 생겼는지를 조금이나마 엿볼 수 있었어서 좋았다.
그리고 일급객체, DI와 같은 개념들도 공부해봐야겠다.

profile
Async FE 취업 준비중.. Await .. (취업완료 대기중) ..

0개의 댓글