개발 공부

React) Controlled/Uncontrolled Component

Ryomi 2025. 1. 22. 13:21
728x90
반응형

 

기본 개념

Controlled Components

- 사용자 입력을 컴포넌트의 state로 관리하는 컴포넌트

- UI에서 입력한 데이터와 저장한 데이터의 상태가 항상 일치

- 변경할 수 있는 state는 일반적으로 컴포넌트의 state 속성에 유지되고 setState()에 의해 업데이트

- 사용자가 입력할 때마다 리렌더링되므로, 실시간으로 값이 필요한 경우 사용

function ControlledInput() {
  const [value, setValue] = React.useState("");

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

 

Uncontrolled Components

- React가 아닌 DOM 자체에서 상태를 관리하는 컴포넌트

- 입력값을 제어하지 않고, 필요할 때 ref를 사용해 값을 가져와 사용

- 기존의 바닐라 js와 유사하며, form을 제출할 때 실행되는 함수에서 값을 얻어올 수 있음

- 불필요한 리렌더링을 줄이고, 특정 시점에만 값을 사용할 수 있음

function UncontrolledInput() {
  const inputRef = React.useRef();

  const handleSubmit = () => {
    console.log(inputRef.current.value);
  };

  return <input type="text" ref={inputRef} />;
}

 

적용하기 - Search bar

Controlled Component

- 사용자 입력에 따라 실시간으로 URL 업데이트

- debouncing을 사용해 사용자 입력값이 변경된 후 일정 시간 경과 후 URL 업데이트

"use client";

import { cn } from "../utils/style";
import { Input } from "../ui/input";
import { SearchIcon } from "lucide-react";
import { useState } from "react";
import { usePathname, useRouter, useSearchParams } from "next/navigation";
import { useDebouncedCallback } from "use-debounce";

export type TypeSearchbarProps = {
  placeholder: string;
  iconPosition: "left" | "right";
  wrapperClassName?: string;
};

export const Searchbar = ({
  placeholder,
  iconPosition,
  wrapperClassName,
}: TypeSearchbarProps) => {
  const searchParams = useSearchParams();
  const keyword = searchParams.get("keyword") || "";

  const pathname = usePathname();
  const router = useRouter();

  // 입력값을 useState로 관리하여 React state와 동기화
  const [search, setSearch] = useState(keyword);

  // 디바운싱된 URL 업데이트 함수
  const updateURL = useDebouncedCallback((value: string) => {
    const params = new URLSearchParams();
    if (value) {
      params.set("keyword", value);
    }
    router.push(`${pathname}?${params.toString()}`);
  }, 300);

  // 입력값 변경 핸들러
  const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const value = e.target.value;
    
    setSearch(value); // React state 업데이트
    updateURL(value); // 디바운싱된 URL 업데이트 호출
  };

  return (
    <div
      className={cn(
        `flex items-center gap-2 px-2 border border-gray-200 rounded-md w-full has-[:focus]:border-gray-500`,
        iconPosition === "right" && "flex-row-reverse",
        wrapperClassName
      )}
    >
      <label htmlFor="search">
        <SearchIcon className="w-4 h-4 text-gray-500" />
      </label>
      <Input
        id="search"
        type="text"
        placeholder={placeholder}
        value={search} // Controlled Component: React state와 연결
        onChange={handleInputChange} // 입력값 변경 핸들러
        className="border-none shadow-none focus:outline-none focus:border-transparent"
        style={{
          boxShadow: "none",
        }}
      />
    </div>
  );
};

 

Uncontrolled Component

- 사용자 입력 후 'Enter key'를 눌렀을 때 URL 변경

- 상태를 관리하지 않으므로 

"use client";

import { cn } from "../utils/style";
import { Input } from "../ui/input";
import { SearchIcon } from "lucide-react";
import { useRef } from "react";
import { usePathname, useRouter, useSearchParams } from "next/navigation";

export type TypeSearchbarProps = {
  placeholder: string;
  iconPosition: "left" | "right";
  wrapperClassName?: string;
};

export const Searchbar = ({
  placeholder,
  iconPosition,
  wrapperClassName,
}: TypeSearchbarProps) => {
  const searchbarRef = useRef<HTMLInputElement>(null);

  const searchParams = useSearchParams();
  const keyword = searchParams.get("keyword") || "";

  const pathname = usePathname();
  const router = useRouter();

  const handleSearch = (e: React.KeyboardEvent<HTMLInputElement>) => {
    const search = searchbarRef.current?.value;

    if (search && e.key === "Enter") {
      const params = new URLSearchParams();
      params.set("keyword", search);
      router.push(`${pathname}?${params.toString()}`);
    }

    return;
  };

  return (
    <div
      className={cn(
        `flex items-center gap-2 px-2 border border-gray-200 rounded-md w-full has-[:focus]:border-gray-500,`,
        iconPosition === "right" && "flex-row-reverse",
        wrapperClassName
      )}
    >
      <label htmlFor="search">
        <SearchIcon className="w-4 h-4 text-gray-500" />
      </label>
      <Input
        ref={searchbarRef} // 값 참조
        id="search"
        type="text"
        placeholder={placeholder}
        defaultValue={keyword} // 기본값 추가
        className="border-none shadow-none focus:outline-none focus:border-transparent"
        style={{
          boxShadow: "none",
        }}
        onKeyDown={handleSearch}
      />
    </div>
  );
};

 

728x90
반응형

'개발 공부' 카테고리의 다른 글

주식 투자와 AI의 만남: Global Stock Analyst GPTs 개발기  (0) 2024.12.08
Chrome 확장 프로그램 - JSON Viewer  (0) 2023.02.16
AWS 청구서  (0) 2023.02.06