React 멘탈 모델은 React를 더 단순하게 느끼게 합니다: 컴포넌트, 렌더링, 상태, 이펙트의 핵심 아이디어를 배우고 이를 적용해 채팅으로 빠르게 UI를 구축하세요.

처음에는 React가 답답하게 느껴질 수 있습니다. UI가 바뀌는 건 보이는데 왜 바뀌었는지 설명할 수 없을 때가 많습니다. 버튼을 클릭하면 뭔가 업데이트되고, 다른 부분이 이상하게 반응해서 놀랄 때가 있죠. 그건 보통 “React가 이상하다”가 아니라 “내가 React가 무엇을 하는지에 대한 그림이 흐리다”는 신호입니다.
멘탈 모델은 어떤 것이 어떻게 동작하는지에 대해 스스로에게 들려주는 단순한 이야기입니다. 이야기가 틀리면 혼자서 확신을 가지고 내린 결정이 혼란스러운 결과로 이어집니다. 온도 조절기를 예로 들면: 나쁜 모델은 “22°C로 설정하면 방이 즉시 22°C가 된다”입니다. 더 나은 모델은 “목표를 정하면 히터가 켜졌다 꺼지며 시간에 걸쳐 목표에 도달한다”입니다. 이 더 좋은 이야기를 알면 동작이 무작위처럼 느껴지지 않습니다.
React도 마찬가지입니다. 몇 가지 명확한 아이디어를 머릿속에 두면 React는 예측 가능해집니다. 현재 데이터를 보면 화면에 무엇이 나올지 신뢰할 수 있게 추측할 수 있습니다.
Dan Abramov는 이런 "예측 가능하게 만들기" 마인드셋을 널리 알리는 데 큰 역할을 했습니다. 목표는 규칙을 외우는 것이 아니라, 디버깅할 때 시행착오가 아니라 추론으로 문제를 해결할 수 있게 몇 가지 진리를 머릿속에 유지하는 것입니다.
다음 아이디어들을 기억하세요:
이 개념들을 잡으면 React는 마법이 아니라 신뢰할 수 있는 시스템처럼 느껴집니다.
화면(스페이스)을 생각하는 대신 작은 조각으로 생각하면 React가 쉬워집니다. 컴포넌트는 재사용 가능한 UI 단위입니다. 입력을 받아 그 입력에 대한 UI 설명을 반환합니다.
컴포넌트를 순수한 설명으로 다루는 것이 도움이 됩니다: "이 데이터가 주어지면 이렇게 보여줘." 그 설명은 어디에 있든 재사용할 수 있습니다.
Props는 입력입니다. 부모 컴포넌트에서 옵니다. Props는 컴포넌트가 소유한 것이 아니며 컴포넌트가 조용히 변경해서는 안 됩니다. 예를 들어 버튼에 label="Save"가 전달되면 버튼의 역할은 그 라벨을 렌더하는 것이지 임의로 바꾸는 것이 아닙니다.
State는 소유된 데이터입니다. 컴포넌트가 시간에 따라 기억하는 것입니다. 사용자가 상호작용하거나 요청이 끝나거나 어떤 것이 달라져야 한다고 결정되면 상태가 바뀝니다. Props와 달리 state는 그 컴포넌트(또는 소유할 컴포넌트)에 속합니다.
핵심 아이디어의 단순한 버전: UI는 state의 함수입니다. state가 "loading"이라면 스피너를 보여주고, "error"라면 메시지를 보여주고, "items = 3"이라면 세 줄을 렌더하세요. UI는 숨은 변수로 흘러들어가지 않고 state를 읽도록 만드세요.
개념을 구분하는 빠른 방법:
SearchBox, ProfileCard, CheckoutForm)name, price, disabled)isOpen, query, selectedId)예: 모달. 부모는 title과 onClose를 props로 줄 수 있습니다. 모달은 isAnimating 같은 상태를 소유할 수 있습니다.
심지어 채팅으로 UI를 생성할 때(예: Koder.ai에서)에도 이 분리는 가장 빠르게 정신을 유지하는 방법입니다: 먼저 무엇이 props이고 무엇이 state인지 결정한 다음 UI를 따르게 하세요.
머릿속에 React를 유지하는 유용한 방식(정신적으로 Dan Abramov 스타일)은: 렌더는 계산이지 페인트 작업이 아니라는 것입니다. React는 현재 props와 state에 대해 컴포넌트 함수를 실행해서 UI가 어떻게 보여야 하는지 계산합니다. 출력은 픽셀이 아니라 UI 설명입니다.
리렌더는 단지 그 계산을 반복하는 것입니다. "페이지 전체를 다시 그린다"는 뜻이 아닙니다. React는 새 결과를 이전 것과 비교하고 실제 DOM에 최소한의 변경만 적용합니다. 많은 컴포넌트가 리렌더되더라도 실제로 업데이트되는 DOM 노드는 몇 개뿐일 수 있습니다.
대부분의 리렌더는 몇 가지 단순한 이유로 발생합니다: 컴포넌트의 state가 바뀌었거나 props가 바뀌었거나 부모가 리렌더되어 자식에게 렌더를 요청할 때입니다. 마지막 경우가 사람들을 놀라게 하지만 보통 괜찮습니다. 렌더를 "값싸고 지루한 것(cheap and boring)"으로 취급하면 앱을 이해하기 쉬워집니다.
이것을 깔끔하게 유지하는 경험칙: 렌더를 순수하게 만드세요. 같은 입력(props + state)이 주어지면 컴포넌트는 같은 UI 설명을 반환해야 합니다. 렌더에서 놀라운 동작을 제거하세요.
구체적 예: 렌더 안에서 Math.random()으로 ID를 생성하면 리렌더마다 값이 바뀌어 체크박스가 포커스를 잃거나 리스트 항목이 다시 마운트될 수 있습니다. ID는 한 번 생성해서(state, memo, 또는 컴포넌트 외부) 렌더를 안정적으로 만드세요.
한 문장만 기억하세요: 리렌더는 "UI가 무엇이어야 하는지 다시 계산한다"이지 "모든 것을 다시 만든다"가 아닙니다.
도움이 되는 모델 하나: 상태 업데이트는 요청(request)이지 즉시 할당(assign)이 아닙니다. setCount(count + 1)처럼 호출하면 React에 새로운 값으로 렌더를 스케줄해 달라고 요청하는 것입니다. 바로 뒤에서 state를 읽으면 아직 이전 값을 볼 수 있습니다(React가 아직 렌더하지 않았기 때문입니다).
그래서 "작고 예측 가능한" 업데이트가 중요합니다. 현재 값을 바로 잡아 쓰기보다는 변경을 설명하는 방법을 선호하세요. 다음 값이 이전 값에 의존하면 업데이터 형태를 사용하세요: setCount(c => c + 1). React의 동작과 일치합니다: 여러 업데이트가 큐에 쌓이고 순서대로 적용될 수 있습니다.
불변성(immutability)은 그림의 다른 절반입니다. 객체와 배열을 제자리에서 변경하지 마세요. 변경을 반영한 새 객체/배열을 만드세요. 그러면 React는 "이 값이 새것이다"라고 감지할 수 있고, 무엇이 바뀌었는지 추적하기 쉬워집니다.
예: todo 항목 토글. 안전한 방법은 새 배열과 변경한 항목에 대한 새 todo 객체를 생성하는 것입니다. 위험한 방법은 기존 배열 안에서 todo.done = !todo.done처럼 수정하는 것입니다.
또한 state는 최소한으로 유지하세요. 흔한 함정은 계산할 수 있는 값을 저장하는 것입니다. 이미 items와 filter가 있다면 filteredItems를 state에 저장하지 마세요. 렌더 중에 계산하세요. 상태 변수가 적을수록 값들이 엇갈릴 가능성이 적습니다.
무엇이 state에 들어가야 할지 간단한 테스트:
채팅으로 UI를 빌드할 때(예: Koder.ai 포함), 변경을 작은 패치로 요청하세요: "불리언 플래그 하나 추가" 또는 "이 리스트를 불변하게 업데이트"처럼. 작고 명시적인 변경은 생성기와 당신의 React 코드가 일치하도록 돕습니다.
렌더링은 UI를 설명합니다. 이펙트는 외부 세계와 동기화합니다. "외부"는 React가 제어하지 않는 것들입니다: 네트워크 호출, 타이머, 브라우저 API, 때로는 명령형 DOM 작업 등.
무언가가 props와 state에서 계산할 수 있다면 보통 이펙트에 둘 이유가 없습니다. 이펙트에 넣으면 두 단계(렌더 → 이펙트 실행 → state 설정 → 다시 렌더)가 추가됩니다. 이 추가 단계가 깜빡임, 루프, "왜 이것이 오래된 값인가?" 버그가 발생하는 곳입니다.
흔한 혼란 예: firstName과 lastName이 있는데 fullName을 이펙트로 만들어 state에 저장하는 경우. fullName은 사이드 이펙트가 아니라 파생 데이터입니다. 렌더 중에 계산하면 항상 일치합니다.
습관으로: UI 값은 렌더 중에(또는 진짜로 비싼 계산이면 useMemo로) 유도하고, 이펙트는 "무언가를 수행"할 때(네트워크 호출, 타이머, 구독 등)만 사용하세요.
의존성 배열은: "이 값들이 바뀔 때 외부와 다시 동기화하라"는 의미로 다루세요. 성능 트릭도 아니고 경고를 무마하는 장소도 아닙니다.
예: userId가 바뀔 때 사용자 상세를 가져오려면 userId는 의존성에 포함되어야 합니다. 이펙트가 token을 사용한다면 token도 포함하세요. 빠뜨리면 오래된 토큰으로 요청할 수 있습니다.
좋은 직관적 점검: 이펙트를 제거했을 때 UI만 틀려진다면 아마 그건 진짜 이펙트가 아닙니다. 제거했을 때 타이머가 멈추거나 구독이 취소되거나 fetch가 중단된다면 그 이펙트는 진짜 이펙트일 가능성이 큽니다.
가장 유용한 멘탈 모델 중 하나는 단순합니다: 데이터는 트리를 따라 아래로 흐르고, 사용자 행동은 위로 올라갑니다.
부모는 자식에게 값을 전달합니다. 자식은 같은 값을 두 곳에서 몰래 소유하면 안 됩니다. 변경을 요청하려면 함수를 호출하고, 부모가 새 값을 결정합니다.
UI의 두 부분이 일치해야 한다면 한 곳에 값을 저장하고 내려주세요. 이것이 "state를 끌어올리기(lifting state)"입니다. 약간의 배선이 추가된다고 느껴질 수 있지만, 더 나쁜 문제(두 상태가 어긋나고 이를 맞추기 위해 해킹을 하는 상황)를 예방합니다.
예: 검색 상자와 결과 리스트. 만약 입력이 자체적으로 query를 저장하고 리스트도 자체적으로 query를 저장하면 결국 "입력은 X인데 리스트는 Y를 사용한다"는 상황을 보게 됩니다. 해결책은 부모에 query를 두고 둘에 전달하며 입력에는 onChangeQuery(newValue) 핸들러를 전달하는 것입니다.
상태 끌어올리기가 항상 정답은 아닙니다. 값이 한 컴포넌트 내부에서만 중요하면 그 안에 두는 것이 맞습니다. 상태를 사용하는 곳 가까이에 보관하면 코드 읽기가 쉬워지는 경우가 많습니다.
실용적 경계:
상태를 끌어올릴지 망설여진다면 이런 신호를 보세요: 두 컴포넌트가 다른 방식으로 같은 값을 보여준다; 한 곳의 액션이 멀리 있는 것을 업데이트해야 한다; props를 "혹시 모르니" state에 복사한다; 두 값을 맞추기 위해 이펙트를 추가한다—이런 경우 소유자를 하나로 정하세요.
이 모델은 채팅 도구(Koder.ai 등)를 사용할 때도 도움이 됩니다: 공유 상태마다 한 명의 소유자를 정하고, 변경 핸들러는 위로 올리게 하세요.
머릿속에 담을 수 있을 만큼 작은 기능을 선택하세요. 좋은 예는 검색 가능한 목록에서 항목을 클릭하면 모달로 상세를 보는 기능입니다.
먼저 UI 부분과 발생할 수 있는 이벤트를 스케치하세요. 아직 코드는 생각하지 마세요. 사용자가 할 수 있는 일과 볼 수 있는 것을 생각하세요: 검색 입력, 리스트, 선택된 행 하이라이트, 모달이 있습니다. 이벤트는 입력에 글자 입력, 항목 클릭, 모달 열기, 모달 닫기입니다.
다음으로 "상태를 그리세요." 저장해야 할 값 몇 개를 적고 누가 소유할지 결정하세요. 좋은 규칙 하나: 값을 필요로 하는 모든 곳의 가장 가까운 공통 부모가 그것을 소유해야 합니다.
이 기능의 저장 상태는 작습니다: query(문자열), selectedId(id 또는 null), isModalOpen(불리언). 리스트는 query를 읽어 항목을 렌더합니다. 모달은 상세를 보여주기 위해 selectedId를 읽습니다. 리스트와 모달 둘 다 selectedId를 필요로 하면 부모에 두세요, 둘 다에 두지 마세요.
다음으로 파생 데이터와 저장 데이터를 분리하세요. 필터된 리스트는 파생입니다: filteredItems = items.filter(...). 상태에 저장하지 마세요. 항상 items와 query에서 재계산할 수 있습니다. 파생 데이터를 저장하면 값들이 어긋나기 쉽습니다.
그다음에 이펙트가 필요한지 물어보세요. 항목이 이미 메모리에 있으면 필요 없습니다. 쿼리 입력 시 서버 요청을 해야 한다면 필요합니다. 모달 닫을 때 뭔가를 저장해야 하면 필요합니다. 이펙트는 동기화용입니다(fetch, save, subscribe), 기본 UI 배선용이 아닙니다.
끝으로 몇 가지 엣지 케이스로 흐름을 점검하세요:
selectedId가 여전히 유효한가?종이 위에서 이 질문들에 답할 수 있다면 React 코드는 보통 직관적입니다.
대부분의 React 혼란은 문법 때문이 아닙니다. 코드가 머릿속의 단순한 이야기와 일치하지 않을 때 발생합니다.
파생 상태 저장. fullName을 state에 저장하는데 실제로는 firstName + lastName인 경우입니다. 한 필드만 바뀌면 UI가 오래된 값을 보여줍니다.
이펙트 루프. 이펙트가 데이터를 가져와 상태를 설정하고 의존성 때문에 다시 실행되는 경우입니다. 증상은 반복 요청, UI 떨림, 상태가 정착하지 않음입니다.
오래된 클로저(stale closures). 클릭 핸들러가 오래된 값을 읽습니다(예: 지난 카운터나 필터). 증상은 "클릭했는데 어제 값이 사용된다"입니다.
전역 상태 남발. 모든 UI 세부를 전역 스토어에 넣으면 누가 무엇을 소유하는지 알기 어려워집니다. 증상은 한 가지를 바꾸면 세 화면이 이상하게 반응하는 것입니다.
중첩된 객체 변이. 객체나 배열을 제자리에서 변경하고 왜 UI가 업데이트되지 않는지 궁금해합니다. 증상은 "데이터는 바뀌었는데 리렌더가 안 된다"입니다.
구체적 예: 검색 및 정렬 패널. filteredItems를 state에 저장하면 새 데이터가 도착했을 때 items와 어긋날 수 있습니다. 대신 입력(검색 텍스트, 정렬 선택)을 저장하고 렌더에서 필터링하세요.
이펙트는 외부와 동기화할 때만 유지하세요(fetching, subscriptions, timers). UI 관련 기본 작업을 이펙트에 넣으면 종종 렌더나 이벤트로 옮겨야 합니다.
채팅으로 코드를 생성하거나 편집할 때 이런 실수는 더 빨리 드러납니다. 변경이 큰 덩어리로 올 수 있기 때문입니다. 좋은 습관은 요청을 소유권 관점으로 구성하는 것입니다: "이 값의 진짜 소스는 어디인가?" 그리고 "저장할 수 있는 대신 계산할 수는 없는가?"
**UI = f(state, props)**로 생각하는 것이 좋은 출발점입니다. 컴포넌트는 DOM을 "편집"하는 것이 아니라 주어진 데이터에 대해 화면에 무엇을 보여줘야 하는지 설명합니다. 화면이 잘못 보인다면 DOM이 아니라 해당 UI를 만든 state/props를 검사하세요.
Props는 부모로부터 오는 입력값입니다; 컴포넌트는 이를 읽기 전용으로 다뤄야 합니다. State는 그 컴포넌트(또는 소유자로 정한 컴포넌트)의 기억입니다. 값이 공유되어야 한다면 부모로 올려서(props로 내려주고 콜백으로 위로 보냄) 처리하세요.
리렌더는 React가 컴포넌트 함수를 다시 실행해서 다음 UI 설명을 계산한다는 뜻입니다. 페이지 전체가 자동으로 다시 그려진다는 의미는 아닙니다. React는 이전 결과와 비교해 실제 DOM에 필요한 최소한의 변경만 적용합니다.
상태 업데이트는 즉시 대입되는 값이 아니라 스케줄된 요청입니다. 다음 값이 이전 값에 의존할 때는 업데이트 함수의 업데이터 형태를 사용하세요:\n\n- setCount(c => c + 1)\n\n이 방식은 여러 업데이트가 대기 중이어도 올바르게 동작합니다.
기존 입력으로 계산할 수 있는 값은 저장하지 마세요. 입력(예: items, filter)을 저장하고 나머지는 렌더에서 파생하세요.\n\n예:\n\n- 저장: items, filter\n- 파생: visibleItems = items.filter(...)\n\n이렇게 하면 값들이 어긋나는 것을 막을 수 있습니다.
이펙트는 React가 제어하지 않는 외부와 동기화할 때 사용하세요: 네트워크 호출, 구독, 타이머, 브라우저 API, 그리고 때로는 명령형 DOM 작업 등.\n\n렌더로부터 계산할 수 있는 UI 값을 굳이 이펙트에서 계산하지 마세요(비용이 크면 useMemo로 최적화).
의존성 배열은 “이 값들이 바뀌면 다시 동기화하라”는 트리거 목록으로 생각하세요. 성능용 트릭이 아니고 경고를 잠재우는 곳도 아닙니다.\n\n예: userId가 바뀔 때 사용자 정보를 가져오려면 userId를 포함해야 하고, 토큰을 사용한다면 token도 포함해야 합니다. 빠뜨리면 오래된 값으로 요청할 위험이 있습니다.
두 UI 부분이 항상 같은 값을 보여야 한다면 그 값의 가장 가까운 공통 부모가 상태를 소유하게 하세요. 값을 내려주고 변경은 콜백으로 올려받습니다.\n\n신호: 같은 값을 둘 곳에 복사해서 쓰고 이를 맞추기 위해 이펙트로 동기화하고 있다면 소유권을 올려야 할 때입니다.
핸들러가 이전 렌더의 오래된 값을 참조해 발생합니다. 일반적인 해결책:\n\n- 업데이터 셋터 사용: setX(prev => ...)\n- 변하는 값은 평범한 변수에 두지 말고 state나 ref에 두기\n- 이펙트에 읽는 값을 의존성으로 포함하기\n\n클릭이 “어제의 값”을 사용한다면 stale closure(오래된 클로저)를 의심하세요.
작은 계획부터 시작하세요: 컴포넌트, 상태 소유자, 이벤트를 먼저 정리한 뒤 코드를 생성합니다. 큰 덩어리로 생성하기보다 작은 패치(상태 한 필드 추가, 핸들러 하나 추가, 파생값 한 개 도출)로 진행하면 상태가 지저분해질 가능성이 적습니다.\n\n채팅 빌더(예: Koder.ai)를 쓸 땐 각 상태 값의 소유자를 명확히 하고, 파생값은 렌더에서 계산하고, 이펙트는 동기화 용도로만 쓰라고 요청하세요.