이제껏 개발을 하면서 컴포넌트가 둘로 구분되는지 모르고 있었는데 리액트 스터디를 통해 확실하게 개념을 정리해볼 수 있었다. 학습한 내용을 바탕으로 클론코딩 중인 코드에 Debounce를 적용해보았고 그 과정을 기록해보았다.
제어? 비제어 ? 컴포넌트
React의 폼데이터를 다루는 두 가지 방식 중 하나인 제어 컴포넌트 방식입니다.
폼 데이터를 다루는 데는 제어컴포넌트와 비제어 컴포넌트 방식 두 가지로 특징을 잘 알아야 상황에 따라 폼관리를 최적화할 수 있습니다.
이름 그대로 값이 제어되는 컴포넌트가 제어 컴포넌트, 제어되지 않는 컴포넌트를 비제어 컴포넌트라고 합니다.
순서대로 코드와 함께 개념을 살펴보겠습니다!
제어 컴포넌트
간단히 말해 React가 폼 데이터를 제어하는 방식입니다.
- HTML에서
<input> <textarea> <select>
와 같은 폼 요소는 일반적으로 사용자의 입력으르 기반으로 state를 관리하고 업데이트 합니다. - React에서는 변경할 수 있는 state가 일반적으로 컴포넌트의 state 속성에 유지되며 setState()에 의해 업데이트됩니다.
- 다음은 제어 컴포넌트의 예시 코드입니다.
import React from 'react';
import { useEffect, useState } from 'react';
const ControlledForm = () => {
const [name, setName] = useState();
const [text, setText] = useState();
useEffect(() => {
console.log('name rendered', name);
}, [name]);
useEffect(() => {
console.log('text rendered', text);
}, [text]);
return (
<>
<label for="name">이름: </label>
<input
name="name"
type="text"
value={name}
onChange={(e) => {
setName(e.target.value);
}}
/>
<label for="text">설명: </label>
<input
name="text"
type="text"
value={text}
onChange={(e) => {
setText(e.target.value);
}}
/>
</>
);
};
export default ControlledForm;

- useEffect로 각각의 input값이 재렌더링 될 때 콘솔을 찍어보면, 사용자가 입력할 때마다 렌더링이 발생하는 것을 볼 수 있습니다.
- 제어컴포넌트는 사용자의 입력값과 저장되는 값이 실시간으로 동기화됩니다.
장점
- 동기화: 사용자 인터페이스와 상태 간의 실시간 데이터 동기화를 보장합니다.
- 예측 가능성: 데이터 흐름과 상태 관리가 예측 가능합니다.
이러한 장점으로 다음과 같을 때 사용됩니다.
- 실시간 유효성 검사, 예를 들어 비밀번호 재입력이 비밀번호와 같은지 실시간으로 보여주고 싶을 때
- 조건에 따라 버튼이 비활성화 되어야 하는 경우, 예를 들어 폼의 모든 입력 값을 받아야 제출 버튼을 활성화 시키고 싶을 때
문제점
코드의 결과이미지로 볼 수 있듯이, 데이터 동기화가 계속되기 때문에 불필요한 렌더링이 발생한다는 점이다. 이는 input 값의 입력이 바뀔 때마다 API호출을 하는 구현을 한다면 API 자원 낭비로 이어질 수 있다.
보완점 => 디바운싱과 쓰로틀링 (Debouncing & Throttling)
- 디바운싱: 연이어 호출되는 함수들 중 마지막 함수 또는 처음 함수만 호출되도록 함.
- 쓰로틀링: 마지막 함수가 호출된 후 일정 시간이 지나기 전에 닫시 호출되지 않도록 하는 것.
디바운싱은 주로 검색 api 호출 시에 사용합니다.
- 예를 들어, "테스트"라는 글자를 입력할 시, setState함수를
'ㅌ','테','텟','테스','테슽','테스트'
총 6번 호출합니다. - 위와 같이 호출을 하게 될 경우, '텟'이나 '테슽'에 대한 결과가 없을 수 있으며 불필요한 렌더링을 하게됩니다.
- 디바운싱은 이를 '테스트' 즉, 마지막 함수만 실행되도록 해줍니다.
쓰로틀링은 주로 스크롤을 올리거나 내릴 때 사용합니다. 이벤트 함수가 스크롤이벤트를 계속 듣고있어 이벤트가 너무 많이 발생합니다. 쓰로틀링을 적용한다면 일정 시간 간격을 두어 실행에 제한을 둘 수 있습니다.
디바운스 적용 예시: 검색창 컴포넌트, URL 연동
다음 코드는 디바운스를 적용하지 않고 제어 컴포넌트를 사용했을 경우입니다.

'use client';
import { IoIosSearch } from 'react-icons/io';
import useDebounce from 'hooks/useDebounce';
import { useEffect, useState } from 'react';
import { useRouter } from 'next/navigation';
function SearchInput() {
const [inputValue, setInputValue] = useState('');
const router = useRouter();
useEffect(() => {
const url = new URL(location.href);
if (inputValue.length > 0) url.searchParams.set('keyword', inputValue);
else {
url.searchParams.delete('keyword');
}
router.replace(url.toString(), undefined);
}, [inputValue]);
return (
<div className="my-3 bg-white overflow-hidden py-3 flex items-center border rounded border-box-border focus-within:border-elice-purple ">
<IoIosSearch className="mx-4 text-text-black" size="16px" />
<input
className="flex-1 text-sm placeholder:text-[gray] outline-none"
type="text"
placeholder="배우고 싶은 언어, 기술을 검색해보세요"
onChange={e => setInputValue(e.target.value)}
/>
</div>
);
}
export default SearchInput;
해당 코드는 inputValue값이 변할 때마다 useEffect로 감지한 후, inputValue를 URL에 연동시켜주는 코드입니다.
결과물처럼 값을 입력할 때마다 실시간으로 URL에 연동된 것을 볼 수 있습니다.
이를 디바운스 hook을 만들어준 후, 사용자의 입력이 다 끝났을 때 요청을 보내주도록 만들겠습니다.
먼저 결과물입니다.

타입스크립트 코드로 보겠습니다.
useDebounce.ts 훅
'use client';
import { useState, useEffect } from 'react';
export default function useDebounce<T>(value: T, delay: number): T {
const [debouncedValue, setDebouncedValue] = useState<T>(value); //입력된 값을 디바운스 된 최종 값으로 저장
useEffect(() => {
const handler = setTimeout(() => {
console.log("setTimer",value)
setDebouncedValue(value);
}, delay);
// 입력값이 변경될 때마다 delay 시간 후 debouncedValue 업데이트
return () => {
clearTimeout(handler);
console.log("Time out")
};
}, [value, delay]);
return debouncedValue;
}
SearchInput.tsx
'use client';
import { IoIosSearch } from 'react-icons/io';
import useDebounce from 'hooks/useDebounce';
import { useEffect, useState } from 'react';
import { useRouter } from 'next/navigation';
function SearchInput() {
const [inputValue, setInputValue] = useState('');
const debouncedSearch = useDebounce(inputValue, 300); //훅 호출
const router = useRouter();
useEffect(() => {
const url = new URL(location.href);
if (inputValue.length > 0) url.searchParams.set('keyword', inputValue);
else {
console.log(inputValue);
url.searchParams.delete('keyword');
}
router.replace(url.toString(), undefined);
}, [debouncedSearch]);
return (
<div className="my-3 bg-white overflow-hidden py-3 flex items-center border rounded border-box-border focus-within:border-elice-purple ">
<IoIosSearch className="mx-4 text-text-black" size="16px" />
<input
className="flex-1 text-sm placeholder:text-[gray] outline-none"
type="text"
placeholder="배우고 싶은 언어, 기술을 검색해보세요"
onChange={e => setInputValue(e.target.value)}
/>
</div>
);
}
export default SearchInput;
동작의 이해를 돕기위해 콘솔로 찍어보았습니다.

- 타자를 치는 동안 (input이벤트 발생-> value값 변경) 마다 useEffect로 setTimeout()으로 타이머를 설정합니다.
- delay시간 후에
debouncedValue
상태를 업데이트 합니다. 만약, 'value'값이 변경되어useEffect
가 실행된다면 useEffect의 clean-up 함수 내의clearTimeout
을 실행하여 이전 타이머를 취소한 후, setTimeout()으로 타이머를 설정합니다. - 위의 예시에 적용한다면. "테스트용"을 연속으로 입력하는 동안 각각 "ㅌ", "테", "텟" ... 입력에 대해 value값이 변화하기 때문에 useEffect가 트리거됩니다.
- useEffect 내 클린업 함수가 실행되며 이전 타이머를 취소합니다 -> console.log("Time out")
- useEffect 내 새로운
setTimeout
이 설정됩니다. - delay 시간 내 사용자 입력이 일어난다면 setTimeout 내 콜백함수는 동작하지 않고 useEffect가 트리거되면서 클린업함수로 이전 타이머가 취소된 후 새로운 타이머가 설정됩니다.
- 더 이상의 입력이 없고, delay 시간이 지나면 마지막으로 설정된 setTimeout 내 콜백함수가 동작하여 debouncedValue값이 업데이트 됩니다.
- 최종적으로 useDebounce 훅을 사용하는 컴포넌트에 debouncedValue값이 반환됩니다.
비제어 컴포넌트
비제어 컴포넌트는 React가 폼 데이터 상태를 직접적으로 관리하지않고, DOM 자체가 폼 데이터를 관리합니다.
React의 ref
를 사용하여 직접 DOM 요소에 접근하고, 필요할 때 그 값들을 추출합니다.

화면처럼 입력 때마다 동기화되지도 않습니다. 즉, 값이 업데이트 될 때마다 리렌더링이 되지 않고 버튼을 눌렀을 때만 API를 호출할 수 있도록 하여 성능상에 이점을 가져다 줄 수 있습니다.
import React, { useRef } from 'react';
const UnControlledForm = () => {
// 입력 필드에 대한 ref를 생성합니다.
const nameRef = useRef(null);
const textRef = useRef(null);
// 폼 제출 핸들러
const handleSubmit = (event) => {
// 폼의 기본 제출 동작을 방지합니다.
event.preventDefault();
// 각 입력 필드의 현재 값에 접근합니다.
const name = nameRef.current.value;
const text = textRef.current.value;
// 입력된 값을 콘솔에 출력합니다.
console.log('이름:', name);
console.log('설명:', text);
};
return (
<form onSubmit={handleSubmit}>
<label htmlFor="name">이름: </label>
<input ref={nameRef} name="name" type="text" />
<br />
<label htmlFor="text">설명: </label>
<input ref={textRef} name="text" type="text" />
<br />
<button type="submit">제출</button>
</form>
);
};
export default UnControlledForm;
비교 표
기능 | 제어 컴포넌트 | 비제어 컴포넌트 |
---|---|---|
일회성 정보 검색 (예: 제출) | O | O |
제출 시 값 검증 | O | O |
실시간으로 필드값의 유효성 검사 | O | X |
조건부로 제출 버튼 비활성화 (disabled) | O | X |
실시간으로 입력 형식 적용하기 (숫자만 가능하게 등) | O | X |
동적 입력 | O | X |
인용
리액트 공식문서
https://www.zerocho.com/category/JavaScript/post/59a8e9cb15ac0000182794fa
'웹개발 > React.js' 카테고리의 다른 글
리액트 State와 Ref | 리액트 공식문서 기반 정리 (1) | 2024.02.06 |
---|---|
리액트 클래스 컴포넌트 라이프 사이클 정리 & 브라우저의 렌더링과 엮어 설명, 리액트에서 Reflow와 Repaint (1) | 2024.02.06 |
리액트 axios 액셀 다운로드 기능 구현. (0) | 2023.12.27 |
리액트 외부영역 클릭 감지 드롭다운 구현하기- 이벤트 버블링과 stopPropagation. (0) | 2023.12.07 |
이제껏 개발을 하면서 컴포넌트가 둘로 구분되는지 모르고 있었는데 리액트 스터디를 통해 확실하게 개념을 정리해볼 수 있었다. 학습한 내용을 바탕으로 클론코딩 중인 코드에 Debounce를 적용해보았고 그 과정을 기록해보았다.
제어? 비제어 ? 컴포넌트
React의 폼데이터를 다루는 두 가지 방식 중 하나인 제어 컴포넌트 방식입니다.
폼 데이터를 다루는 데는 제어컴포넌트와 비제어 컴포넌트 방식 두 가지로 특징을 잘 알아야 상황에 따라 폼관리를 최적화할 수 있습니다.
이름 그대로 값이 제어되는 컴포넌트가 제어 컴포넌트, 제어되지 않는 컴포넌트를 비제어 컴포넌트라고 합니다.
순서대로 코드와 함께 개념을 살펴보겠습니다!
제어 컴포넌트
간단히 말해 React가 폼 데이터를 제어하는 방식입니다.
- HTML에서
<input> <textarea> <select>
와 같은 폼 요소는 일반적으로 사용자의 입력으르 기반으로 state를 관리하고 업데이트 합니다. - React에서는 변경할 수 있는 state가 일반적으로 컴포넌트의 state 속성에 유지되며 setState()에 의해 업데이트됩니다.
- 다음은 제어 컴포넌트의 예시 코드입니다.
import React from 'react';
import { useEffect, useState } from 'react';
const ControlledForm = () => {
const [name, setName] = useState();
const [text, setText] = useState();
useEffect(() => {
console.log('name rendered', name);
}, [name]);
useEffect(() => {
console.log('text rendered', text);
}, [text]);
return (
<>
<label for="name">이름: </label>
<input
name="name"
type="text"
value={name}
onChange={(e) => {
setName(e.target.value);
}}
/>
<label for="text">설명: </label>
<input
name="text"
type="text"
value={text}
onChange={(e) => {
setText(e.target.value);
}}
/>
</>
);
};
export default ControlledForm;

- useEffect로 각각의 input값이 재렌더링 될 때 콘솔을 찍어보면, 사용자가 입력할 때마다 렌더링이 발생하는 것을 볼 수 있습니다.
- 제어컴포넌트는 사용자의 입력값과 저장되는 값이 실시간으로 동기화됩니다.
장점
- 동기화: 사용자 인터페이스와 상태 간의 실시간 데이터 동기화를 보장합니다.
- 예측 가능성: 데이터 흐름과 상태 관리가 예측 가능합니다.
이러한 장점으로 다음과 같을 때 사용됩니다.
- 실시간 유효성 검사, 예를 들어 비밀번호 재입력이 비밀번호와 같은지 실시간으로 보여주고 싶을 때
- 조건에 따라 버튼이 비활성화 되어야 하는 경우, 예를 들어 폼의 모든 입력 값을 받아야 제출 버튼을 활성화 시키고 싶을 때
문제점
코드의 결과이미지로 볼 수 있듯이, 데이터 동기화가 계속되기 때문에 불필요한 렌더링이 발생한다는 점이다. 이는 input 값의 입력이 바뀔 때마다 API호출을 하는 구현을 한다면 API 자원 낭비로 이어질 수 있다.
보완점 => 디바운싱과 쓰로틀링 (Debouncing & Throttling)
- 디바운싱: 연이어 호출되는 함수들 중 마지막 함수 또는 처음 함수만 호출되도록 함.
- 쓰로틀링: 마지막 함수가 호출된 후 일정 시간이 지나기 전에 닫시 호출되지 않도록 하는 것.
디바운싱은 주로 검색 api 호출 시에 사용합니다.
- 예를 들어, "테스트"라는 글자를 입력할 시, setState함수를
'ㅌ','테','텟','테스','테슽','테스트'
총 6번 호출합니다. - 위와 같이 호출을 하게 될 경우, '텟'이나 '테슽'에 대한 결과가 없을 수 있으며 불필요한 렌더링을 하게됩니다.
- 디바운싱은 이를 '테스트' 즉, 마지막 함수만 실행되도록 해줍니다.
쓰로틀링은 주로 스크롤을 올리거나 내릴 때 사용합니다. 이벤트 함수가 스크롤이벤트를 계속 듣고있어 이벤트가 너무 많이 발생합니다. 쓰로틀링을 적용한다면 일정 시간 간격을 두어 실행에 제한을 둘 수 있습니다.
디바운스 적용 예시: 검색창 컴포넌트, URL 연동
다음 코드는 디바운스를 적용하지 않고 제어 컴포넌트를 사용했을 경우입니다.

'use client';
import { IoIosSearch } from 'react-icons/io';
import useDebounce from 'hooks/useDebounce';
import { useEffect, useState } from 'react';
import { useRouter } from 'next/navigation';
function SearchInput() {
const [inputValue, setInputValue] = useState('');
const router = useRouter();
useEffect(() => {
const url = new URL(location.href);
if (inputValue.length > 0) url.searchParams.set('keyword', inputValue);
else {
url.searchParams.delete('keyword');
}
router.replace(url.toString(), undefined);
}, [inputValue]);
return (
<div className="my-3 bg-white overflow-hidden py-3 flex items-center border rounded border-box-border focus-within:border-elice-purple ">
<IoIosSearch className="mx-4 text-text-black" size="16px" />
<input
className="flex-1 text-sm placeholder:text-[gray] outline-none"
type="text"
placeholder="배우고 싶은 언어, 기술을 검색해보세요"
onChange={e => setInputValue(e.target.value)}
/>
</div>
);
}
export default SearchInput;
해당 코드는 inputValue값이 변할 때마다 useEffect로 감지한 후, inputValue를 URL에 연동시켜주는 코드입니다.
결과물처럼 값을 입력할 때마다 실시간으로 URL에 연동된 것을 볼 수 있습니다.
이를 디바운스 hook을 만들어준 후, 사용자의 입력이 다 끝났을 때 요청을 보내주도록 만들겠습니다.
먼저 결과물입니다.

타입스크립트 코드로 보겠습니다.
useDebounce.ts 훅
'use client';
import { useState, useEffect } from 'react';
export default function useDebounce<T>(value: T, delay: number): T {
const [debouncedValue, setDebouncedValue] = useState<T>(value); //입력된 값을 디바운스 된 최종 값으로 저장
useEffect(() => {
const handler = setTimeout(() => {
console.log("setTimer",value)
setDebouncedValue(value);
}, delay);
// 입력값이 변경될 때마다 delay 시간 후 debouncedValue 업데이트
return () => {
clearTimeout(handler);
console.log("Time out")
};
}, [value, delay]);
return debouncedValue;
}
SearchInput.tsx
'use client';
import { IoIosSearch } from 'react-icons/io';
import useDebounce from 'hooks/useDebounce';
import { useEffect, useState } from 'react';
import { useRouter } from 'next/navigation';
function SearchInput() {
const [inputValue, setInputValue] = useState('');
const debouncedSearch = useDebounce(inputValue, 300); //훅 호출
const router = useRouter();
useEffect(() => {
const url = new URL(location.href);
if (inputValue.length > 0) url.searchParams.set('keyword', inputValue);
else {
console.log(inputValue);
url.searchParams.delete('keyword');
}
router.replace(url.toString(), undefined);
}, [debouncedSearch]);
return (
<div className="my-3 bg-white overflow-hidden py-3 flex items-center border rounded border-box-border focus-within:border-elice-purple ">
<IoIosSearch className="mx-4 text-text-black" size="16px" />
<input
className="flex-1 text-sm placeholder:text-[gray] outline-none"
type="text"
placeholder="배우고 싶은 언어, 기술을 검색해보세요"
onChange={e => setInputValue(e.target.value)}
/>
</div>
);
}
export default SearchInput;
동작의 이해를 돕기위해 콘솔로 찍어보았습니다.

- 타자를 치는 동안 (input이벤트 발생-> value값 변경) 마다 useEffect로 setTimeout()으로 타이머를 설정합니다.
- delay시간 후에
debouncedValue
상태를 업데이트 합니다. 만약, 'value'값이 변경되어useEffect
가 실행된다면 useEffect의 clean-up 함수 내의clearTimeout
을 실행하여 이전 타이머를 취소한 후, setTimeout()으로 타이머를 설정합니다. - 위의 예시에 적용한다면. "테스트용"을 연속으로 입력하는 동안 각각 "ㅌ", "테", "텟" ... 입력에 대해 value값이 변화하기 때문에 useEffect가 트리거됩니다.
- useEffect 내 클린업 함수가 실행되며 이전 타이머를 취소합니다 -> console.log("Time out")
- useEffect 내 새로운
setTimeout
이 설정됩니다. - delay 시간 내 사용자 입력이 일어난다면 setTimeout 내 콜백함수는 동작하지 않고 useEffect가 트리거되면서 클린업함수로 이전 타이머가 취소된 후 새로운 타이머가 설정됩니다.
- 더 이상의 입력이 없고, delay 시간이 지나면 마지막으로 설정된 setTimeout 내 콜백함수가 동작하여 debouncedValue값이 업데이트 됩니다.
- 최종적으로 useDebounce 훅을 사용하는 컴포넌트에 debouncedValue값이 반환됩니다.
비제어 컴포넌트
비제어 컴포넌트는 React가 폼 데이터 상태를 직접적으로 관리하지않고, DOM 자체가 폼 데이터를 관리합니다.
React의 ref
를 사용하여 직접 DOM 요소에 접근하고, 필요할 때 그 값들을 추출합니다.

화면처럼 입력 때마다 동기화되지도 않습니다. 즉, 값이 업데이트 될 때마다 리렌더링이 되지 않고 버튼을 눌렀을 때만 API를 호출할 수 있도록 하여 성능상에 이점을 가져다 줄 수 있습니다.
import React, { useRef } from 'react';
const UnControlledForm = () => {
// 입력 필드에 대한 ref를 생성합니다.
const nameRef = useRef(null);
const textRef = useRef(null);
// 폼 제출 핸들러
const handleSubmit = (event) => {
// 폼의 기본 제출 동작을 방지합니다.
event.preventDefault();
// 각 입력 필드의 현재 값에 접근합니다.
const name = nameRef.current.value;
const text = textRef.current.value;
// 입력된 값을 콘솔에 출력합니다.
console.log('이름:', name);
console.log('설명:', text);
};
return (
<form onSubmit={handleSubmit}>
<label htmlFor="name">이름: </label>
<input ref={nameRef} name="name" type="text" />
<br />
<label htmlFor="text">설명: </label>
<input ref={textRef} name="text" type="text" />
<br />
<button type="submit">제출</button>
</form>
);
};
export default UnControlledForm;
비교 표
기능 | 제어 컴포넌트 | 비제어 컴포넌트 |
---|---|---|
일회성 정보 검색 (예: 제출) | O | O |
제출 시 값 검증 | O | O |
실시간으로 필드값의 유효성 검사 | O | X |
조건부로 제출 버튼 비활성화 (disabled) | O | X |
실시간으로 입력 형식 적용하기 (숫자만 가능하게 등) | O | X |
동적 입력 | O | X |
인용
리액트 공식문서
https://www.zerocho.com/category/JavaScript/post/59a8e9cb15ac0000182794fa
'웹개발 > React.js' 카테고리의 다른 글
리액트 State와 Ref | 리액트 공식문서 기반 정리 (1) | 2024.02.06 |
---|---|
리액트 클래스 컴포넌트 라이프 사이클 정리 & 브라우저의 렌더링과 엮어 설명, 리액트에서 Reflow와 Repaint (1) | 2024.02.06 |
리액트 axios 액셀 다운로드 기능 구현. (0) | 2023.12.27 |
리액트 외부영역 클릭 감지 드롭다운 구현하기- 이벤트 버블링과 stopPropagation. (0) | 2023.12.07 |