개발새발 로그

[2024-02-11] 개인 프로젝트 회고 - 검색과 추천검색어, 디바운싱, 제어컴포넌트와 비제어 컴포넌트 본문

TIL

[2024-02-11] 개인 프로젝트 회고 - 검색과 추천검색어, 디바운싱, 제어컴포넌트와 비제어 컴포넌트

이즈흐 2024. 2. 11. 02:21

팀프로젝트에 NextJS 공부에 묻혀서 개인프로젝트를 소홀히 해버렸다.

얼른 다시 개인 프로젝트 1일 1커밋을 위해 노력할 것이다...ㅠㅠ

 

 

1. 검색 API를 이용해서 검색을 하자

이제 검색 API를 이용해서 검색어에 해당하는 약초 데이터를 불러올 것이다.

공공데이터에서 제공해준 문서를 보면 아래와 같다.

기존에 약초리스트를 모두 불러올 때는 sType값과 sText값을 쿼리에 넣지않고 보냈다.

이번에는 sType과 sText를 넣어서 요청을 하면 검색이 가능할 것이라고 예상된다.

 

1. 먼저 서버 라우터를 구축을 해야했다.

이전에 만들었던 서버의 router에 '/serach' url을 추가해줬다.

router.get('/search', async (req, res) => {
  try {
    const sText = req.query.sText
    const response = await axios.get(url, {
      params: {
        apiKey,
        sType: 'sCntntsSj',
        sText
      }
    })

    const xmlToJson = converter.xml2json(response.data)
    res.send(xmlToJson)
  } catch (error) {
    console.error(error)
    res.status(500).send('Internal Server Error')
  }
})

먼저 sType은 명칭으로 고정을 했다. (sCntntsSj가 명칭임)

그리고 sText는 요청을 보낼 때 params 안에 넣어서 보내줄 것이다.

 

2. 이제 axios요청을 보내보자

axios요청은 이전에 리스트를 불러왔던 요청과 거의 유사했다.

import axios from 'axios'

export const getSearchData = async (searchData: string) => {
  const response = await axios.get('http://localhost:5000/api/search', {
    params: {
      sText: searchData
    }
  })
  return response.data.elements[0].elements[1].elements[0].elements
}

이제 이 함수를 그냥 요청하기만 하면 끝이다

리액트 쿼리는 이후에 적용하려고 한다!

const result = await getSearchData(query)

 

2. 검색 API 요청을 이용해서 추천 검색어 기능을 넣어보자

검색어 기능을 만들면서 추천 검색어 기능을 만들고 싶은 생각이 마구 들었다.

왜냐하면 이 사이트를 사용하는 사용자가 약초의 이름을 얼마나 알겠는가?!
이를 위해서는 추천 검색어가 꼭 있어야 된다고 판단했다.

 

그래서 나는 Input 태그에 타이핑을 할 때마다 API요청을 보내서 해당되는 모든 약초를 불러오고,

그 약초이름을 추천검색어에 띄우려고 했다.

 

근데 만약 현재 공공데이터 농사로에서 지원하는 검색 API가 "가*" 과 같이 "가"라는 글자가 포함되는 약초를 모두 검색해올지는 미지수였다.

 

다행이도 지원을 하고 있었다.

"가"를 검색하면 "가"를 포함하는 약초를 불러왔다.

 

일단 추천검색어를 하기 위해 조건을 생각해야했다.

1. null값일 때는 요청못하도록 하기

2. API 요청을 받아온 데이터에서 내가 필요한 약초이름만 추출하기

 

이 두 조건을 먼저 충족해야했다.

그래서 아래와 같이 코드를 구성했다.

const [recommendList, setRecommendList] = useState([])
  const [searchQuery, setSearchQuery] = useState<string>('')
const [recommendList, setRecommendList] = useState([])
    
  const handleChange = async (e: ChangeEvent<HTMLInputElement>) => {
    const query = e.target.value.trim()
    setSearchQuery(query)

    if (query === '') {
      return
    }

    const result = await getSearchData(query)
    const recommendData = result
      .filter((data: HerbInfos) => data.name === 'item')
      .map((data: HerbInfos) => {
        return {
          No: Number(data.elements[1].elements[0].cdata),
          name: data.elements[2].elements[0].cdata
        }
      })
    setRecommendList(recommendData)
 }
    
    return (
    <SearchInputContainer>
      <form onSubmit={handleSubmit}>
      	<Input
          type="text"
          name="search"
          value={searchQuery}
          onChange={debouncedOnChange}
          placeholder="약초를 검색해보세요."
        />
        <button type="submit">
          <SvgContainer>
            <svg
              xmlns="http://www.w3.org/2000/svg"
              className="icon icon-tabler icon-tabler-search svelte-1leehxl"
              width="24"
              height="24"
              viewBox="0 0 24 24"
              stroke="currentColor"
              fill="none">
              <path
                stroke="none"
                d="M0 0h24v24H0z"
                fill="none"></path>
              <path d="M10 10m-7 0a7 7 0 1 0 14 0a7 7 0 1 0 -14 0"></path>
              <path d="M21 21l-6 -6"></path>
            </svg>
          </SvgContainer>
        </button>
      </form>
      {recommendList.length > 0 && (
        <RecommendListUl>
          <span>추천 검색어</span>
          {recommendList.map((data: RecommendListItem) => (
            <RecommendListLi key={data.No}>
              {data.name}
            </RecommendListLi>
          ))}
        </RecommendListUl>
      )}
    </SearchInputContainer>
  )
    
  }

위와 같이 구성해서 타이핑을 할 때마다 API요청을 하고 그 값을 state에 저장해 map으로 뿌려주었다.

 

여기서 궁금증

보통 input 태그의 value에 state값을 지정하고 onChange로 하는 방식을 쓴다.

근데 사실 검색이라고하면 검색어를 입력하고 엔터나 검색 아이콘을 클릭했을 때

e.target.value로 값을 갖고와서 API요청할 때 넣어주면 됐다.

 

그래서 "굳이 state를 하나 생성해서 input에 연결해주고 그 값을 관리하는게 좋은건가?" 라고 생각했다.

 

이 궁금증을 해결하기위해 검색을 했고, 이 부분을 설명하는 것이 많이 있었다.

바로 controlled component uncontorlled component다.

https://dori-coding.tistory.com/entry/React-%EC%A0%9C%EC%96%B4-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8Controlled-Component%EC%99%80-%EB%B9%84%EC%A0%9C%EC%96%B4-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8Uncontrolled-Component

 

[React] 제어 컴포넌트(Controlled Component)와 비제어 컴포넌트(Uncontrolled Component)

React에서는 Form을 다루는 2가지 방법이 있는데, 바로 제어 컴포넌트(Controlled Component)와 비제어 컴포넌트(Uncontrolled Component)이다. 과연 제어 컴포넌트(Controlled Component)와 비제어 컴포넌트(Uncontrolled

dori-coding.tistory.com

 

Uncontrolled component한마디로 표현하자면, state를 사용하지 않는 방식이라고 말할 수 있다.

state를 사용하지 않겠다는 의미는 곧 input에 사용자 입력 이벤트가 발생해도 그 값을 React 시스템 안에 저장하지 않겠다는 것이기 때문에 Input의 변경을 추적하지 않는다. 따라서, 값이 변경되어도 렌더링이 트리거되지 않는다는 특징이 있다.

 

그럼 controlled componentInput의 값을 state를 이용해서 동기화한다. 사용자 입력을 React 시스템에서 관리하겠다는 의미로, 사용자 입력 시 렌더링을 트리거하는 특징이 있다.

 

그래서 어떤 걸 사용하라는 거지?

 

옛날 리액트의 공식문서에서는 아래와 같이 말하고 있다.

이게 왜 그렇까 해서 찾아봤더니 아래와 같은 점때문이라고 한다.

  1. 단일 출처(Source of Truth): 입력 값이 React 상태에 의해 제어되면 해당 상태가 입력 값의 '진리의 원천(Source of Truth)'이 됩니다. 상태와 뷰가 일치하므로 코드가 예측 가능하고 유지 보수가 쉽습니다. 어디서든지 현재 상태를 확인할 수 있습니다. 즉, 언제나 해당 상태를 통해 현재 값에 접근할 수 있습니다.
  2. 데이터 일관성: React 상태가 input 값과 일치하면서 상태가 변경되면 React는 자동으로 해당 변경사항을 input에 반영합니다. 이렇게 함으로써 입력 값과 상태 간에 일관성을 유지할 수 있습니다.
  3. React의 성능 최적화:: React의 가상 돔(Virtual DOM) 및 재조정 알고리즘을 활용하여 성능 향상 및 최적화를 수행할 수 있습니다.
  4. 테스트 용이성: 상태와 뷰가 명확히 매핑되어 있으므로 테스트 작성이 더 쉽습니다
  5. React 생태계의 다른 기능과의 통합성: 다른 React 기능과의 통합이 더 쉽습니다. 예를 들어, React Hooks와 함께 사용할 때 더 자연스러운 통합이 가능합니다.

챗GPT는 아래와 같이 말하고 있다.

Controlled components를 사용하지 않는 경우 (Uncontrolled components)에는 특별한 상황이나 퍼포먼스 최적화 등의 이유가 있을 수 있습니다.
그러나 대부분의 경우, React의 철학에 부합하고 코드를 더 예측 가능하고 유지보수하기 쉽게 만들기 위해 controlled components를 사용하는 것이 좋습니다.
더 간단한 프로토타이핑이나 특정한 상황에서는 Uncontrolled components를 사용할 수 있지만, 일반적으로 controlled components를 지향하는 것이 React에서의 권장 사항입니다.

 

그래도 블로그를 작성한 분들은 다들 아래와 같이 말하고 있었다.

❓ 제어 컴포넌트와 비제어 컴포넌트는 어떤 경우에 사용하는 것이 바람직할까
제어 컴포넌트(Controlled Component)는 UI의 입력한 데이터 상태(사용자에게 보여지는 화면)와 저장한 데이터의 상태가 항상 일치한다.
이 말은 즉슨, 사용자가 입력할 때마다 재렌더링되고 있다는 것이다.
그래서 실시간으로 값이 필요할 때는 제어 컴포넌트(Controlled Component)를 사용하고,
불필요한 재렌더링을 줄이고 제출 시에만 값이 필요할 때는 비제어 컴포넌트(UnControlled Component)를 사용하는 것이 좋다.
❓ 제어 컴포넌트를 지향하라는 의견이 많은 이유는 무엇일까요?
비제어 컴포넌트를 사용하면 submit할 때만 value(input)에 접근할 수 있어요.
반면에 제어 컴포넌트는 언제든지 state를 통해 value에 접근할 수 있으므로 실시간 검증이나 입력 강제에 대해서 유연합니다.
왜 uncontrolled와 controlled 컴포넌트에 대해 알아야 할까?
React는 내부의 상태(state)를 '신뢰 가능한 단일 소스(Single Source of Truth)'로 관리하려는 설계 원칙을 가지고 있다.
즉 자식 컴포넌트가 data가 필요할 경우, 해당 data는 가장 가까운 공통 부모 컴포넌트에게서만 props의 형태로 전달받아서 사용해야 한다.
대부분의 HTML 엘리먼트들(ex. <div> 등)은 엘리먼트가 내부적으로 어떤 데이터를 가지지 않기 때문에 문제될 것이 없다.
 
하지만 HTML 엘리멘트 중 자체적으로 특정 data를 가지는 엘리먼트들이 있다.
바로 <form> 태그의 엘리먼트들이다.(<input>, <textarea>, <select> 등)
이들은 user가 DOM에서 어떤 정보를 입력하거나 선택할 경우, 해당 정보를 HTML 엘리먼트가 직접 보관하게 되는데,
이는 위에서 언급한 리액트의 핵심 설계원리인 '신뢰 가능한 단일 소스' 원칙에 위배되는 상황이다.
따라서 이를 해결하기 위해서 React에서 Controlled 컴포넌트의 개념이 나온 것이다.

위 내용을 종합해서 내린 결론은 일단 controlled component 사용을 지향해야한다. 

하지만 controlled component를 사용했을 때 change이벤트로인한 리렌더링 이슈를 항상 생각해야한다는 점이다.

그래서 만약 불필요한 상황에는 uncontrolled component를 사용하는 것도 좋다라고 하는 것 같다.

 

3. 타이핑 할 때마다 API요청을 너무 부담돼!

위에서 작성한 코드에서 문제점은 너무 잦은 API요청을 한다는 것이다.

그래서인지 API요청도 엄청느린 것 같았다.

그래서 디바운싱을 적용하기로 했다.

보통 제어컴포넌트를 사용할 때의 최적화 방법중 하나라고 한다.

(이후에는 공식문서에 나온 최적화방법도 적용해보겠다)

https://lasbe.tistory.com/153

 

[React/TypeScript] Debounce, 일정 시간동안 발생한 이벤트 중 마지막만 실행

⚡Debounce란 Debounce란 일정 시간 동안 연속적으로 발생했던 이벤트들 중 마지막만 실행시켜 과다한 호출이나 렌더를 막아 최적화하는 기술입니다. 예를 들어 이번 개인 프로젝트 중 입력 시 중복

lasbe.tistory.com

디바운싱을 위 블로그를 참고했다.

타입을 유연하게 사용할 수 있어 모듈로서도 좋은 것 같았다.

const useDebounce = <T extends (...args: any[]) => any>(
  fn: T,
  delay: number
) => {
  let timeout: ReturnType<typeof setTimeout>

  return (...args: Parameters<T>): ReturnType<T> => {
    let result: any
    if (timeout) clearTimeout(timeout)
    timeout = setTimeout(() => {
      result = fn(...args)
    }, delay)
    return result
  }
}
export default useDebounce
const handleChange = async (e: ChangeEvent<HTMLInputElement>) => {
    const query = e.target.value.trim()
    setSearchQuery(query)

    if (query === '') {
      return
    }

    const result = await getSearchData(query)
    const recommendData = result
      .filter((data: HerbInfos) => data.name === 'item')
      .map((data: HerbInfos) => {
        return {
          No: Number(data.elements[1].elements[0].cdata),
          name: data.elements[2].elements[0].cdata
        }
      })
    setRecommendList(recommendData)
  }
  const debouncedOnChange = useDebounce<typeof handleChange>(handleChange, 500)

위처럼 handleChange함수에 디바운싱만 추가해주면 됐다.

 

이로서 타이핑이 일정시간 멈췄을 때 API요청을하고 추천검색어를 보여주게됐다!

4. 현재 검색어를 이용해서 추천검색어에 어떤 글자에 해당하는지 표시해서 사용자 경험을 높이자

추천검색어를 출력하고 그걸 스타일링하면서 뭔가 부족하다는 생각이 들었다.

구글 추천검색을 보고나니 알 수 있었다.

내가 검색한 단어가 추천검색어에서 강조가 되어있지 않는 것이었다.

 

그래서 이 기능도 추가해보았다.

const [searchQuery, setSearchQuery] = useState<string>('') // input값 저장하는 state 

const boldSearchQuery = (text: string, query: string): React.ReactNode => {
    const index = text.toLowerCase().indexOf(query.toLowerCase())

    if (index !== -1) {
      return (
        <div style={{ display: 'flex' }}>
          {text.substring(0, index)}
          <strong style={{ color: 'lightgreen' }}>
            {text.substring(index, index + query.length)}
          </strong>
          {text.substring(index + query.length)}
        </div>
      )
    }
    
    ...
    
    {recommendList.length > 0 && (
        <RecommendListUl>
          <span>추천 검색어</span>
          {recommendList.map((data: RecommendListItem) => (
            <RecommendListLi key={data.No}>
              {boldSearchQuery(data.name, searchQuery)}
            </RecommendListLi>
          ))}
        </RecommendListUl>
      )}

boldeSearchQuery라는 함수를 만들어서 현재 text와 현재 검색어 쿼리를 가져와서

해당 되는 문자를 강조시키는 JSX요소로 바꿔주었다.

이렇게 만들고 보니 이쁘게 추천검색어가 출력됐다.

 

 

 

📜참고 자료

제어 컴포넌트와 비제어 컴포넌트의 예시와 어떤 것이 바람직한지

https://dori-coding.tistory.com/entry/React-%EC%A0%9C%EC%96%B4-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8Controlled-Component%EC%99%80-%EB%B9%84%EC%A0%9C%EC%96%B4-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8Uncontrolled-Component

 

[React] 제어 컴포넌트(Controlled Component)와 비제어 컴포넌트(Uncontrolled Component)

React에서는 Form을 다루는 2가지 방법이 있는데, 바로 제어 컴포넌트(Controlled Component)와 비제어 컴포넌트(Uncontrolled Component)이다. 과연 제어 컴포넌트(Controlled Component)와 비제어 컴포넌트(Uncontrolled

dori-coding.tistory.com

왜 제어 컴포넌트에 대해서 알아야하는지

https://soldonii.tistory.com/145

 

200207(금) : Uncontrolled vs. Controlled Component in React

React의 컴포넌트를 설명할 때 Uncontrolled Component와 Controlled Component라는 개념이 있다. 이 둘에 대해서 살펴보자. 1. 왜 uncontrolled와 controlled 컴포넌트에 대해 알아야 할까? React는 내부의 상태(state)를

soldonii.tistory.com

제어컴포넌트를 사용해야하는 이유와 비제어 컴포넌트를 사용할 순간, 그리고 useImperativeHandle

https://velog.io/@jhy979/React-Controlled-Uncontrolled-Components

 

velog

 

velog.io

제어컴포넌트 사용시 최적화 방법이 공식문서에 나와있다!

https://react.dev/reference/react-dom/components/input#optimizing-re-rendering-on-every-keystroke

 

<input> – React

The library for web and native user interfaces

react.dev

제어컴포넌트와 비제어 컴포넌트 구분하고, 제어컴포넌트의 성능이슈 해결방법 제시

https://blog.leaphop.co.kr/blogs/33

 

React Uncontrolled & Controlled Component, 프로처럼 사용하기

React Uncontrolled & Controlled Component에 대해서 제대로 알아봅시다. 실제 예시도 살펴보면서 이제 진짜 프로처럼 React Component를 사용해보세요.

blog.leaphop.co.kr

 

728x90
반응형
LIST