React

 

 

소스:

https://github.com/braverokmc79/react-ex27-udemy

 

 

509.컴파운드 컴포넌트 소개

 

  1. Compound Components (합성 컴포넌트):

    • 이 패턴은 React 컴포넌트가 독립적이지 않고 함께 동작하는 것을 의미합니다.
    • HTML의 select와 option 요소처럼 함께 사용되어야만 의미가 있는 요소들이 예시로 제공됩니다.
  2. Accordion Component (아코디언 컴포넌트):

    • 아코디언은 여러 섹션 중 하나를 확장할 수 있는 UI 패턴입니다.
    • 하나의 섹션을 확장하면 다른 섹션이 닫히는 특징이 있습니다.
  3. AccordionItem Component (아코디언 항목 컴포넌트):

    • 각 아코디언 섹션을 구성하는 하위 컴포넌트입니다.
    • 제목과 내용을 표시하며, 클릭하면 해당 섹션이 확장됩니다.
  4. 구현과 스타일링:

    • Accordion 컴포넌트에서 각 섹션을 나타내는 AccordionItem 컴포넌트를 사용합니다.
    • CSS 클래스를 통해 스타일링을 적용하고, 각 항목에 클래스를 추가하여 스타일링을 개선합니다.
  5. 기능 추가:

    • 현재는 각 항목을 클릭해도 확장되지 않으므로, 다음에는 이러한 기능을 추가할 예정입니다.

이와 같은 설명을 통해 React 애플리케이션에서 합성 컴포넌트 패턴을 활용하여 아코디언을 구현하는 방법을 이해할 수 있습니다.

App.jsx

import Accordion from "./components/Accordion/Accordion";
import AccordionItem from "./components/Accordion/AccordionItem";

function App() {
  return (
    <main>
      <section>
        <h1>우리와 협업하는 이유는 무엇일까요?</h1>

        <Accordion className="accordion">
          <AccordionItem
            className="accordion-item"
            title="저희는 20년 이상의 경험을 가지고 있습니다"
          >
            <article>
              <p>우리를 선택하면 실망하지 않으실 겁니다.</p>
              <p>
                저희는 20년 이상 맞춤형 휴가 여행을 계획하는 사업을 하고
                있습니다.
              </p>
            </article>
          </AccordionItem>

          <AccordionItem
            className="accordion-item"
            title="저희는 현지 가이드와 협력하고 있습니다."
          >
            <article>
              <p>우리는 사무실에서만 이 일을 하지 않습니다.</p>
              <p>
                대신, 우리는 현지 가이드와 함께하여 안전한 휴가를 보장합니다.
              </p>
            </article>
          </AccordionItem>
        </Accordion>
      </section>
    </main>
  );
}

export default App;

 

Accordion.jsx

export default  function Accordion({children, className}) {
  return (
    <ul className={className}>
            {children}
    </ul>
  )
}

 

AccordionItem.jsx

export default function AccordionItem({className, title,children}) {
  return (
    <li>
        <h3>{title}</h3>
        {children}
    </li>
  )
}

 

 

 

 

 

 

 

510.컨텍스트 API 로 멀티 컴포넌트 상태 관리하기

 

  1. 기본 설정:

    • 항목들이 기본적으로 열려있지 않도록 하고, 클릭하여 열리고 닫히며, 최대 한 항목만 열리도록 구현됩니다.
  2. Accordion 컴포넌트:

    • React Context를 사용하여 상태 관리를 합니다.
    • AccordionContext를 생성하고, 각 항목의 열린 상태(openItemId)와 이를 조작할 수 있는 함수(openItem, closeItem)를 포함하는 context value 객체를 생성합니다.
    • Accordion 컴포넌트에서 AccordionContext.Provider로 감싸고, context value를 제공합니다.
  3. AccordionItem 컴포넌트:

    • 각 아코디언 항목에 대한 상태와 상호작용을 관리하기 위해 useAccordionContext 커스텀 훅을 생성합니다.
    • 해당 항목이 열려있는지 여부를 확인하고, 클릭 이벤트를 처리하는 로직을 구현합니다.
    • 각 항목은 AccordionContext를 통해 열린 상태를 가져와 UI를 조작합니다.
  4. 사용자 정의 훅 (useAccordionContext):

    • Accordion 컴포넌트와 연관된 모든 컴포넌트에서 AccordionContext를 사용하기 쉽게 하기 위해 사용자 정의 훅을 생성합니다.
    • 이를 통해 해당 컴포넌트가 Accordion 컴포넌트에 의해 래핑되었는지 확인하고, 필요한 context 값을 제공합니다.
  5. 모달 열고 닫기 추가:

    • 클릭 이벤트 핸들러를 통해 각 항목을 열고 닫을 수 있습니다.
    • 항목이 열려있는지 여부에 따라 적절한 CSS 클래스가 적용되어 UI가 업데이트됩니다.

이러한 접근 방식을 통해 아코디언 컴포넌트를 효과적으로 구현하고 관리할 수 있습니다.

 

Accordion.jsx

import { useContext } from "react";
import { useState } from "react";
import { createContext } from "react";

const AccordionContext = createContext();

export function useAccordionContext(){
    const ctx=useContext(AccordionContext);

    if(!ctx){
        throw new Error("아코디언 관련 컴포넌트는 <Accordion>으로 래핑되어야 합니다.");
    }

    return ctx;
}


export default function Accordion({ children, className }) {

  const [openItemId, setOpenItemId]  =useState();

  function openItem(id){
    setOpenItemId(id);
  }


  function closeItem(id){
    setOpenItemId(null);
  }

  const contextValue={
    openItemId,
    openItem,
    closeItem,
  }


  return (
    <AccordionContext.Provider value={contextValue}>
      <ul className={className}>{children}</ul>
    </AccordionContext.Provider>
  );
}

 

 

 

AccordionItem.jsx

import { useAccordionContext } from "./Accordion";

export default function AccordionItem({ id, className, title, children }) {
  const { openItemId, openItem, closeItem } = useAccordionContext();

  const isOpen= openItemId ===id

  function handleClick(){
    if(isOpen){
        closeItem();
    }else{
        openItem(id);
    }
  }

  return (
    <li onClick={handleClick}>
      <h3>{title}</h3>
      <div className={isOpen ? 'accordion-item-content open' :"accordion-item-content"}>{children}</div>
    </li>
  );
}

 

 

 

 

 

 

 

 

511.컴파운드 컴포넌트 그룹화

 

"Accordion-related components must be wrapped by <Accordion>"라는 오류 메시지는 사용자 정의 훅

useAccordionContext에서 검사하는 부분입니다. 이 오류 메시지는 다음과 같은 상황에서 발생합니다:

  1. Accordion 관련 컴포넌트가 Accordion 컴포넌트로 래핑되어 있지 않은 경우.
  2. useAccordionContext 훅을 사용하여 Accordion 관련 컴포넌트에서 AccordionContext를 사용하려고 할 때.

해당 오류 메시지는 개발자에게 Accordion 관련 컴포넌트가 반드시 <Accordion>으로 래핑되어야 한다는 사실을 알려줍니다. 따라서 다음과 같은 조치를 취할 수 있습니다:

  1. Accordion 관련 컴포넌트들을 <Accordion> 컴포넌트 내에 래핑합니다.
  2. useAccordionContext 훅을 사용하는 모든 컴포넌트가 <Accordion> 컴포넌트 내에 있도록 합니다.

이렇게 함으로써 Accordion 컴포넌트와 관련된 모든 컴포넌트가 올바르게 작동하고 필요한 상태 및 함수를 사용할 수 있습니다

 

Accordion.jsx

import { useContext } from "react";
import { useState } from "react";
import { createContext } from "react";
import AccordionItem from "./AccordionItem";

const AccordionContext = createContext();

export function useAccordionContext(){
    const ctx=useContext(AccordionContext);

    if(!ctx){
        throw new Error("아코디언 관련 컴포넌트는 <Accordion>으로 래핑되어야 합니다.");
    }

    return ctx;
}


export default function Accordion({ children, className }) {

  const [openItemId, setOpenItemId]  =useState();


  function toggleItem(id){
    
    setOpenItemId(prevId=>{
        console.log(" toggleItem :",id, prevId===id);
        return    prevId===id? null:id
    });
  }



  const contextValue={
    toggleItem,
    openItemId,
  }


  return (
    <AccordionContext.Provider value={contextValue}>
      <ul className={className}>{children}</ul>
    </AccordionContext.Provider>
  );
}


 Accordion.Item=AccordionItem;

 

 

AccordionItem.jsx

import { useAccordionContext } from "./Accordion";

export default function AccordionItem({ id, className, title, children }) {
  const { openItemId, toggleItem } = useAccordionContext();

  const isOpen= openItemId ===id

  return (
    <li onClick={()=>toggleItem(id)}>
      <h3>{title}</h3>
      <div className={isOpen ? 'accordion-item-content open' :"accordion-item-content"}>{children}</div>
    </li>
  );
}

 

App.jsx

import Accordion from "./components/Accordion/Accordion";

// import AccordionItem from "./components/Accordion/AccordionItem";

function App() {
  return (
    <main>
      <section>
        <h1>우리와 협업하는 이유는 무엇일까요?</h1>

        <Accordion className="accordion">
          <Accordion.Item 
            id="experience"
            className="accordion-item"
            title="저희는 20년 이상의 경험을 가지고 있습니다"
          >
            <article>
              <p>우리를 선택하면 실망하지 않으실 겁니다.</p>
              <p>
                저희는 20년 이상 맞춤형 휴가 여행을 계획하는 사업을 하고
                있습니다.
              </p>
            </article>
          </Accordion.Item >

          <Accordion.Item 
            id="local-guides"
            className="accordion-item"
            title="저희는 현지 가이드와 협력하고 있습니다."
          >
            <article>
              <p>우리는 사무실에서만 이 일을 하지 않습니다.</p>
              <p>
                대신, 우리는 현지 가이드와 함께하여 안전한 휴가를 보장합니다.
              </p>
            </article>
          </Accordion.Item >
        </Accordion>
      </section>
    </main>
  );
}

export default App;

 

 

 

 

 

 

 

 

512.재사용성 밍 구성 가능성을 위한 추가적인 컴포넌트 추가하기

 

 

AccordionContext.jsx

import { useAccordionContext } from "./Accordion";

export default function AccordionContext({ id, className, children }) {
  const { openItemId } = useAccordionContext();
  const isOpen = openItemId === id;

  return (
    <div className={isOpen ? `${className ?? ''} open` : `${className ?? ''} close`}>
      {children}
    </div>
  );
}

 

AccordionTitle.jsx

import { useAccordionContext } from "./Accordion"

export default function AccordionTitle({id, className, children}) {
  const {toggleItem} = useAccordionContext();
  return (
    <h3 className={className}   onClick={()=>toggleItem(id)}>
            {children}
    </h3>
  )
}

 

AccordionItem.jsx

export default function AccordionItem({ className, children }) {
  return <li className={className}> {children}</li>;
}

 

 

 

Accordion.jsx

import { useContext } from "react";
import { useState } from "react";
import { createContext } from "react";
import AccordionItem from "./AccordionItem";
import AccordionTitle from "./AccordionTitle";
import AccordionContext from './AccordionContext';

const AccordionCtx = createContext();

export function useAccordionContext(){
    const ctx=useContext(AccordionCtx);

    if(!ctx){
        throw new Error("아코디언 관련 컴포넌트는 <Accordion>으로 래핑되어야 합니다.");
    }

    return ctx;
}


export default function Accordion({ children, className }) {

  const [openItemId, setOpenItemId]  =useState();


  function toggleItem(id){
    
    setOpenItemId(prevId=>{
        console.log(" toggleItem :",id, prevId===id);
        return    prevId===id? null:id
    });
  }


  const contextValue={
    toggleItem,
    openItemId,
  }


  return (
    <AccordionCtx.Provider value={contextValue}>
      <ul className={className}>{children}</ul>
    </AccordionCtx.Provider>
  );
}


 Accordion.Item=AccordionItem;
 Accordion.Title=AccordionTitle;
 Accordion.Context=AccordionContext;

 

 

App.jsx

import Accordion from "./components/Accordion/Accordion";

// import AccordionItem from "./components/Accordion/AccordionItem";

function App() {
  return (
    <main>
      <section>
        <h1>우리와 협업하는 이유는 무엇일까요?</h1>

        <Accordion className="accordion">
          <Accordion.Item className="accordion-item">
            <Accordion.Title id="experience">
              저희는 20년 이상의 경험을 가지고 있습니다
            </Accordion.Title>
            <Accordion.Context id="experience">
              <article>
                <p>우리를 선택하면 실망하지 않으실 겁니다.</p>
                <p>
                  저희는 20년 이상 맞춤형 휴가 여행을 계획하는 사업을 하고
                  있습니다.
                </p>
              </article>
            </Accordion.Context>
          </Accordion.Item>

          <Accordion.Item className="accordion-item">
            <Accordion.Title id="local-guides">
              저희는 현지 가이드와 협력하고 있습니다.
            </Accordion.Title>
            <Accordion.Context id="local-guides">
              <article>
                <p>우리는 사무실에서만 이 일을 하지 않습니다.</p>
                <p>
                  대신, 우리는 현지 가이드와 함께하여 안전한 휴가를 보장합니다.
                </p>
              </article>
            </Accordion.Context>
          </Accordion.Item>

          
        </Accordion>
      </section>
    </main>
  );
}

export default App;

 

 

 

 

 

 

 

 

 

513.컴파운드 컴포넌트로 작업할 때 교차 컴포넌트 상태 공우-아코디언 최종

 

 

1)App.jsx

import Accordion from "./components/Accordion/Accordion";

// import AccordionItem from "./components/Accordion/AccordionItem";

function App() {
  return (
    <main>
      <section>
        <h1>우리와 협업하는 이유는 무엇일까요?</h1>

        <Accordion className="accordion">
          <Accordion.Item id="experience" className="accordion-item">
            <Accordion.Title >
              저희는 20년 이상의 경험을 가지고 있습니다
            </Accordion.Title>
            <Accordion.Context>
              <article>
                <p>우리를 선택하면 실망하지 않으실 겁니다.</p>
                <p>
                  저희는 20년 이상 맞춤형 휴가 여행을 계획하는 사업을 하고
                  있습니다.
                </p>
              </article>
            </Accordion.Context>
          </Accordion.Item>

          <Accordion.Item id="local-guides" className="accordion-item">
            <Accordion.Title >
              저희는 현지 가이드와 협력하고 있습니다.
            </Accordion.Title>
            <Accordion.Context>
              <article>
                <p>우리는 사무실에서만 이 일을 하지 않습니다.</p>
                <p>
                  대신, 우리는 현지 가이드와 함께하여 안전한 휴가를 보장합니다.
                </p>
              </article>
            </Accordion.Context>
          </Accordion.Item>


        </Accordion>
      </section>
    </main>
  );
}

export default App;

 

 

2)Accordion.jsx

import { useContext } from "react";
import { useState } from "react";
import { createContext } from "react";
import AccordionItem from "./AccordionItem";
import AccordionTitle from "./AccordionTitle";
import AccordionContext from './AccordionContext';

const AccordionCtx = createContext();

export function useAccordionContext(){
    const ctx=useContext(AccordionCtx);

    if(!ctx){
        throw new Error("아코디언 관련 컴포넌트는 <Accordion>으로 래핑되어야 합니다.");
    }
    return ctx;
}


export default function Accordion({ children, className }) {
  const [openItemId, setOpenItemId]  =useState();

  function toggleItem(id){    
    setOpenItemId(prevId=> prevId===id? null:id);
  }


  const contextValue={
    toggleItem,
    openItemId,
  }


  return (
    <AccordionCtx.Provider value={contextValue}>
      <ul className={className}>{children}</ul>
    </AccordionCtx.Provider>
  );
}


 Accordion.Item=AccordionItem;
 Accordion.Title=AccordionTitle;
 Accordion.Context=AccordionContext;

 

 

3)AccordionItem.jsx

import { createContext, useContext } from "react";

const AccordionItemContext = createContext();

export function useAccordionItemContext() {
  const ctx = useContext(AccordionItemContext);
  if(!ctx){
    throw new Error("useAccordionItemContext must be used within an AccordionItem");
  }
  return ctx;
}


export default function AccordionItem({ id, className, children }) {
  return (
    <AccordionItemContext.Provider value={id}>
      <li className={className}>  {children}</li>
    </AccordionItemContext.Provider>
  );
}

 

 

4)AccordionTitle.jsx

import { useAccordionContext } from "./Accordion"
import { useAccordionItemContext } from "./AccordionItem";

export default function AccordionTitle({className, children}) {
  const {toggleItem} = useAccordionContext();
  const id=useAccordionItemContext();
  return (
    <h3 className={className}   onClick={()=>toggleItem(id)}>
            {children}
    </h3>
  )
}

 

 

5)AccordionContext.jsx

import { useAccordionContext } from "./Accordion";
import { useAccordionItemContext } from "./AccordionItem";

export default function AccordionContext({className, children }) {
  const { openItemId } = useAccordionContext();
  const id=useAccordionItemContext();

  const isOpen = openItemId === id;

  return (
    <div className={isOpen ? `${className ?? ''} open` : `${className ?? ''} close`}>
      {children}
    </div>
  );
}

 

 

 

 

 

 

 

 

 

Render Props


 

"Render Props"는 React 컴포넌트 간에 코드를 공유하기 위해 함수 props를 이용하는 간단한 테크닉입니다.
이 패턴은 컴포넌트가 자체적으로 렌더링 로직을 구현하는 대신, React 엘리먼트를 반환하고 이를 호출하는 함수를 사용합니다.

예를 들어, 아래와 같이 DataProvider 컴포넌트를 사용할 수 있습니다.

 

<DataProvider render={data => (
  <h1>Hello {data.target}</h1>
)}/>

 

이 경우, DataProvider 컴포넌트는 render prop으로 전달된 함수를 호출하고,
이 함수는 data를 인자로 받아 React 엘리먼트를 반환합니다.

Render Props 패턴은 React Router, Downshift, Formik 등의 라이브러리에서 사용되며, 
횡단 관심사 (Cross-Cutting Concerns)를 처리하는 데 유용합니다. 
하지만 최근에는 많은 경우에서 Render Props가 custom Hooks에 의해 대체되었습니다.

이 패턴을 사용하면, 컴포넌트 간에 상태나 동작을 쉽게 공유할 수 있습니다. 
예를 들어, 웹 애플리케이션에서 마우스 위치를 추적하는 로직을 캡슐화한 Mouse 컴포넌트를 만들 수 있습니다.
이 Mouse 컴포넌트는 마우스 움직임 이벤트를 감지하고, 마우스 커서의 (x, y) 위치를 저장하는 행위를 캡슐화합니다.
그런 다음, 이 Mouse 컴포넌트를 사용하여 다른 컴포넌트에서 마우스 위치 정보를 쉽게 재사용할 수 있습니다
 

 

 

 

 

 

 

 

 

514.랜더 프롭(Render Props )소개 및 사용하기

 

리액트의 Render Props 패턴을 활용하여 재사용 가능한 검색 가능한 목록 컴포넌트를 만드는 방법을 단계별로 설명하겠습니다.

Render Props 패턴 개요

Render Props 패턴은 컴포넌트가 함수를 children prop으로 전달받아, 해당 함수를 통해 렌더링 로직을 구현하는 패턴입니다. 이 패턴의 핵심은 컴포넌트가 children prop으로 전달된 함수를 호출하여 렌더링 결과를 반환하는 것입니다.

Render Props 패턴 사용 예시

1. 프로젝트 구조 설정

  • components/SearchableList/SearchableList.jsx 파일을 생성합니다.

2. SearchableList 컴포넌트 구현

 

SearchableList.jsx

export default function SearchableList({ items }) {
  return (
    <div className="searchable-list">
      <input type="search" placeholder="검색" />

      <ul>
        {items.map((item, index) => (
          <li key={index}>{item.toString()}</li>
        ))}
      </ul>
    </div>
  );
}

 

  • items prop: 검색 가능한 항목들의 목록.
  • children prop: 각 항목을 렌더링하는 함수.

3. App 컴포넌트 수정

  • App.js 파일을 열고 SearchableList 컴포넌트를 추가합니다.

 

 

      <section>
          <SearchableList  items={PLACES}  />
          <SearchableList  items={["item 1" ,"item 2"]}  />
      </section>
  • PLACES 상수는 더미 데이터입니다.
  • 각 SearchableList 컴포넌트는 다른 데이터를 출력합니다.

4. 더미 데이터와 스타일 추가

  • data.js 파일에 더미 데이터를 정의합니다.

 

import savannaImg from '../../assets/african-savanna.jpg';
import amazonImg from '../../assets/amazon-river.jpg';
import caribbeanImg from '../../assets/caribbean-beach.jpg';
import desertImg from '../../assets/desert-dunes.jpg';
import forestImg from '../../assets/forest-waterfall.jpg';

export const PLACES = [
    {
        id: 'african-savanna',
        image: savannaImg,
        title: '아프리카 사바나',
        description: '자연의 아름다움을 경험하세요.',
      },
      {
        id: 'amazon-river',
        image: amazonImg,
        title: '아마존 강',
        description: '세계에서 가장 큰 강을 알아보세요.',
      },
      {
        id: 'caribbean-beach',
        image: caribbeanImg,
        title: '카리브 해 해변',
        description: '태양과 해변을 즐기세요.',
      },
      {
        id: 'desert-dunes',
        image: desertImg,
        title: '사막 모래언덕',
        description: '사막의 삶을 발견하세요.',
      },
      {
        id: 'forest-waterfall',
        image: forestImg,
        title: '숲 속의 폭포',
        description: '물소리를 들어보세요.',
      }
      
];

 

 

 

요약

  • Render Props 패턴은 컴포넌트가 children prop으로 전달된 함수를 호출하여 렌더링 결과를 반환하는 패턴입니다.
  • SearchableList 컴포넌트는 검색 가능한 항목을 필터링하고, 각 항목을 children prop으로 전달된 함수로 렌더링합니다.
  • App 컴포넌트는 SearchableList 컴포넌트를 사용하여 다양한 데이터 목록을 렌더링합니다.

이 과정을 통해 재사용 가능하고 확장성이 높은 검색 가능한 목록 컴포넌트를 구현할 수 있습니다.

 

 

 

 

 

 

 

516.랜더 프롭을 사용하여 검색 기능 구현하기

Render Props 패턴을 이용한 SearchableList 컴포넌트 구현

Render Props 패턴은 함수형 프로그래밍의 개념을 활용하여 컴포넌트 간의 렌더링 로직을 유연하게 공유할 수 있게 합니다. 이 패턴은 주로 children prop에 함수를 전달하여 사용됩니다.

구현 개요

  1. SearchableList 컴포넌트

    • items prop을 받아 필터링된 목록을 렌더링.
    • children prop을 함수로 받아, 각 항목을 렌더링하는 데 사용.
  2. App 컴포넌트

    • SearchableList 컴포넌트를 두 번 사용하여, 각각 다른 데이터를 렌더링.
    • Place 컴포넌트를 사용하여 항목을 렌더링.

단계별 구현

1. SearchableList 컴포넌트 구현

SearchableList.jsx

import { useState } from "react";

export default function SearchableList({ items,  children}) {

  const [searchTerm, setSearchTerm] =useState('');

  const searchResults=items.filter(item=>{
   if(JSON.stringify(item).toLowerCase().includes(searchTerm.toLowerCase()) ){
        return item;  
    } 
 }); 

  function handleChange(event){
    setSearchTerm(event.target.value);
  }

  return (
    <div className="searchable-list">
      <input type="search" placeholder="검색" onChange={handleChange} />

      <ul>
        {searchResults&& searchResults.map((item, index) => (
          <li key={index}>
                {children(item)}
          </li>
        ))}
      </ul>
    </div>
  );


}

 

 

2. Place 컴포넌트 구현

export default function Place({ item }) {
    return (
      <article className="place">
        <img src={item.image} alt={item.title} />
        <div>
          <h2>{item.title}</h2>
          <p>{item.description}</p>
        </div>
      </article>
    );
  }

 

3. App 컴포넌트 수정

import Accordion from "./components/Accordion/Accordion";
import { PLACES } from "./components/SearchableList/DummyData";
import Place from "./components/SearchableList/Place";
import SearchableList from "./components/SearchableList/SearchableList";

// import AccordionItem from "./components/Accordion/AccordionItem";



function App() {
  return (

~      


<section>
          <SearchableList  items={PLACES} itemKeyFn={(item) =>item.id} >
              {(item)=> <Place item={item} />}
          </SearchableList>
          

          <SearchableList  items={["item 1" ,"item 2"]} itemKeyFn={(item) =>item.id}  >
             {(item)=> item}
          </SearchableList>
      </section>

    </main>
  );
}

export default App;

 

설명

  1. SearchableList 컴포넌트

    • useState를 사용하여 searchTerm 상태를 관리.
    • 입력 필드에서 onChange 이벤트를 통해 searchTerm을 업데이트.
    • items prop에서 searchTerm에 맞는 항목을 필터링.
    • 필터링된 항목을 map을 사용해 순회하면서 children 함수로 렌더링.
  2. App 컴포넌트

    • SearchableList 컴포넌트를 사용하여 PLACES와 문자열 배열을 렌더링.
    • children prop으로 전달된 함수를 통해 각 항목을 렌더링.
    • PLACES 항목은 Place 컴포넌트를 사용하여 렌더링.

요약

Render Props 패턴을 사용하여 컴포넌트의 렌더링 로직을 유연하게 관리할 수 있습니다.

SearchableList 컴포넌트는 필터링된 항목을 children prop으로 전달된 함수로 렌더링하며, 이로 인해 다양한 항목을 유연하게 렌더링할 수 있습니다.

 

 

 

 

 

 

 

517.동적으로 키 처리하기

SearchableList 컴포넌트의 개선: 동적 키 생성

리액트 애플리케이션에서 Render Props 패턴을 활용해 SearchableList 컴포넌트를 개선하고, 각 항목의 고유 키를 동적으로 생성하는 방법에 대해 설명하겠습니다. 이 개선 사항은 item.id가 없는 경우에도 유효한 키를 생성할 수 있게 합니다.

구현 개요

  1. itemKeyFn Prop 추가

    • SearchableList 컴포넌트에 itemKeyFn prop을 추가하여, 항목의 고유 키를 동적으로 생성합니다.
    • itemKeyFn은 각 항목을 받아 고유한 키를 반환하는 함수입니다.
  2. App 컴포넌트에서 itemKeyFn 설정

    • PLACES와 같은 객체 배열에서 item.id를 키로 사용.
    • 문자열 배열에서는 문자열 자체를 키로 사용.

단계별 구현

1. SearchableList 컴포넌트 수정

import { useState } from "react";

export default function SearchableList({ items, itemKeyFn,  children}) {

  const [searchTerm, setSearchTerm] =useState('');

  const searchResults=items.filter(item=>{
   if(JSON.stringify(item).toLowerCase().includes(searchTerm.toLowerCase()) ){
        return item;  
    } 
 }); 

  function handleChange(event){
    setSearchTerm(event.target.value);
  }

  return (
    <div className="searchable-list">
      <input type="search" placeholder="검색" onChange={handleChange} />

      <ul>
        {searchResults&& searchResults.map((item, index) => (
          <li key={itemKeyFn(item)}>
                {children(item)}
          </li>
        ))}
      </ul>
    </div>
  );


}

 

  • itemKeyFn prop을 추가하여 항목의 고유 키를 생성하는 함수로 사용합니다.
  • filteredItems.map에서 각 항목의 키를 itemKeyFn(item)으로 설정합니다.

 

App 컴포넌트 수정

~

function App() {
  return (
~

      <section>
          <SearchableList  items={PLACES} itemKeyFn={(item) =>item.id} >
              {(item)=> <Place item={item} />}
          </SearchableList>
          

          <SearchableList  items={["item 1" ,"item 2"]} itemKeyFn={(item) =>item.id}  >
             {(item)=> item}
          </SearchableList>
      </section>


~

 

  • PLACES 배열을 사용하는 SearchableList에 itemKeyFn prop으로 item.id를 반환하는 함수를 전달합니다.
  • 문자열 배열을 사용하는 SearchableList에 itemKeyFn prop으로 문자열 자체를 반환하는 함수를 전달합니다.

요약

  1. SearchableList 컴포넌트에 itemKeyFn 추가

    • 항목의 고유 키를 동적으로 생성할 수 있는 itemKeyFn prop을 추가하여, 각 항목의 고유 키를 설정합니다.
  2. App 컴포넌트에서 itemKeyFn 설정

    • PLACES 배열과 같은 객체 배열에서는 item.id를 키로 사용하고, 문자열 배열에서는 문자열 자체를 키로 사용합니다.

이 개선을 통해 SearchableList 컴포넌트는 항목의 키를 동적으로 생성할 수 있어, 고유한 키가 없거나 각기 다른 데이터 구조를 가진 항목들에서도 유효한 키를 생성할 수 있습니다. 이는 Render Props 패턴을 확장하여 다양한 상황에서도 재사용 가능한 컴포넌트를 만드는 좋은 예시입니다.

 

 

 

 

 

 

 

 

518.디바운싱 작업하기

 

Debouncing 구현 예제

다음은 Debouncing을 통해 SearchableList 컴포넌트에서 검색 기능을 최적화하는 코드입니다. 키 입력마다 검색이 이루어지지 않고, 일정 시간 동안 입력이 멈추면 검색이 실행되도록 합니다.

SearchableList 컴포넌트 수정

import { useRef, useState } from "react";

export default function SearchableList({ items, itemKeyFn,  children}) {

  const [searchTerm, setSearchTerm] =useState('');

  const lastChange=useRef();

  const searchResults=items.filter(item=>{
   if(JSON.stringify(item).toLowerCase().includes(searchTerm.toLowerCase()) ){
        return item;  
    } 
 }); 

  function handleChange(event){
    const newSearchTerm = event.target.value;

    // 기존 타이머를 제거합니다.
    if (lastChange.current) {
      clearTimeout(lastChange.current);
    }

    // 새로운 타이머를 설정합니다.
    lastChange.current = setTimeout(() => {
      setSearchTerm(newSearchTerm);
      lastChange.current = null; // 타이머 만료 후 타이머 id를 초기화합니다.
    }, 500);    
  }

  

  return (
    <div className="searchable-list">
      <input type="search" placeholder="검색" onChange={handleChange}   />

      <ul>
        {searchResults&& searchResults.map((item, index) => (
          <li key={itemKeyFn(item)}>
                {children(item)}
          </li>
        ))}
      </ul>
    </div>
  );


}

 

설명

  1. useRef 사용: lastChange ref를 사용하여 마지막 타이머 ID를 저장합니다.
  2. setTimeout 사용: handleChange 함수에서 setTimeout을 사용하여 500ms 후에 searchTerm을 업데이트합니다.
  3. 기존 타이머 취소: 새로운 타이머를 설정하기 전에 clearTimeout을 사용하여 기존 타이머를 취소합니다.
  4. searchResults 필터링: searchTerm이 변경되면 items 배열을 필터링하여 검색 결과를 생성합니다.

 

최종 결과

Debouncing을 통해 성능을 최적화하고, 사용자가 일정 시간 동안 입력을 멈출 때만 검색이 실행되도록 합니다.

이는 HTTP 요청을 줄이거나 복잡한 로직이 포함된 검색 기능의 성능을 크게 향상시킬 수 있습니다.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

about author

PHRASE

Level 60  라이트

내 형은 새들에게 용서를 빌었다. 이러한 것은 보기에 따라서는 아무 의미가 없겠지만, 사실 그의 행동은 옳은 것이다. 왜냐하면 모든 것은 대해(大海)와 같아서, 모두가 흐르면서 서로가 닿고 있기 때문에 세계의 한 끝에서 당신이 몸짓을 하면 바로 반대쪽으로 그 반향(反響)이 울려올 것이다. -도스토예프스키 [카라마죠프형제들]

댓글 ( 0)

댓글 남기기

작성