개발새발 로그

React - 아코디언 기능을 만들 때 어떻게 해야할까? 본문

React

React - 아코디언 기능을 만들 때 어떻게 해야할까?

이즈흐 2024. 5. 31. 19:50

📖들어가며...

저번 포스팅에서 GNB를 만들 때 아코디언을 활용해서 만들었었다.

그 때는 아코디언이 열림과 닫힘 상태를 현재 경로를 활용하고,

닫혀있을 때는 css에서 height값을 0으로 주고, 열렸을 때 height값의 변경으로 열리면서 transiton 효고를 넣었다.

(height로 transition을 주는 것이 아쉽긴 했다.)

 

근데 아코디언으로 만들면서 "리액트로 아코디언을 만들 때 많은 경우의 수가 있는 것 같은데" 라고 생각이 들었다.

그래서 이번에 리액트 UI 모음을 만들면서 이와 같은 상황들을 공부하고, 직접 만들어보려고 한다.

 

만드는 과정은 간단하게 정리만하고, 코드는 자세하게 올리지 않으려고 한다.

강의 영상이나 검색 자료를 보면서 나의 주관적인 생각을 정리하면서 포스팅하려한다!

 

 

🤔아코디언을 만들려면 어떻게 해야할까?

 처음에 리액트로 아코디언을 만든다고 했을 때 무엇을 활용해서 아코디언을 만들까??

 

1.🛠️useState 사용 및 조건부 렌더링으로 구현

보통은 useState를 사용해 상태로 아코디언의 열림과 닫힘 상태를 관리할 것으로 짐작한다.

그리고 조건부 렌더링을 통해서 구현하면 정말 간단하게 구현할 수 있다.

const [currentId, setCurrentId] = useState<string | null>(data[0].id)

const toggleItem = (id: string) => () => {
setCurrentId(prev => (prev === id ? null : id))
}

//...

// 아코디언 요소들
{data.map(d => (
  <AccordionItem
    {...d}
    key={d.id}
    current={currentId === d.id}
    toggle={toggleItem(d.id)}
  />
))}

//...

{current ? (
<AccordionDescription>{description}</AccordionDescription>
) : null}

코드를 많이 잘라내긴했지만 어떻게 작동되는지 짐작할 수 있을 것이다.

✨+보너스 상식!

지금 위 코드에 생소한 부분이 있을 것이다.

const toggleItem = (id: string) => () => {
	setCurrentId(prev => (prev === id ? null : id))
}

바로 이 코드다.

무슨 코드인지 아는 사람도 있을 것이다.

바로 클로저를 이용한 함수이다.

 

🤔왜 클로저일까?

클로저를 사용안하고 코드를 작성해보겠다.

// 기본적인 함수
const toggleItem = (id: string) => {
    setCurrentId(prev => (prev === id ? null : id))
  }
 
 //...
 
 //toogleItem 함수를 하위 컴포넌트에 "전달"
 {data.map(d => (
  <AccordionItem
    {...d}
    toggle={toggleItem} 
  />
))}

// ...

const AccordionItem = ({
  toggle
  ...나머지 props
}: {
  toggle: (id:string) => void
  ...나머지 props
}) => {
	//...
    
	<AccordionTab
        onClick={()=>toggle(id)}>
        {title}
      </AccordionTab>
}

위에서부터 차례로 보면 toggleItem 함수를 그대로 전달하고,

하위 컴포넌트에서 인자로 현재 id를 넣어주는 모습이다.

 

그럼 다시 클로저를 활용한 함수를 보자

  const toggleItem = (id: string) => () => {
    setCurrentId(prev => (prev === id ? null : id))
  }
  
 // ...
  
toggle={toggleItem(d.id)}
   
// ...

//props 부분
toggle: () => void})=>{

// ...

 onClick={toggle}>

하위 컴포넌트의 toggle props에 함수를 전달하기 전에

id값을 인자로 넣어서 실행한 반환 값(함수)을 toggle에 props로 내려주었다.

 

그럼 하위 컴포넌트는 어떤 함수를 반환 받는데 

이때 클로저로 인해 실행했던 함수의 렉시컬 스코프에 있던 인자 id를 기억하게 된다.

그래서 아래 toggle에서 인자를 할당하지 않아도 정상적으로 사용할 수 있는 것이다.

 

클로저를 사용함으로 코드가 간결해지고, 하위 컴포넌트는 더욱 순수한 컴포넌트가 되었다.

 

 


 

2.🛠️useState 사용 및 CSS 로 구현

주관적으로 생각했을 때 조건부 렌더링을 사용하면 좋지않은 점이 2가지가 있다. 

  1. transition 효과를 주기 어렵다.(요소가 실제로 없어졌다가 나타나는 거라서)
  2. useState와 같은 상태가 필요하다.

이번에는 첫 번째에서 말한 좋지 않은 점을 개선해보려고한다.

조건부 렌더링 대신에 CSS를 이용해서 요소가 없어지고 나타나게 하는 것이다.

그리고 CSS를 이용해서 안보이게하고 보이게 하기 때문에 transition 효과를 줄 수 있게 된다.

 

나는 styled-component를 사용했기 때문에 current라는 값을 넘겨줘서 스타일링 해주었다.

/*실제 보이지 말아야할 description 부분*/
<AccordionDescription current={current}>
    {description}
</AccordionDescription>

/* styled-component */
 
 export const AccordionDescription = styled.div<AccordionPropsCurrent>`
  background-color: #eff;
  overflow: hidden;
  transition: all 0.3s;
  ${props =>
    props.current
      ? css`
          padding: 15px;
          border-bottom: 1px solid #ccc;
          max-height: 100vh;
        `
      : css`
          padding: 0 15px;
          border-bottom: 0;
          max-height: 0;
        `}
`

간단하게 current를 받아와서 css 값을 조건부로 받게 했다.

만약 sass,scss 를 사용했을 때는 .curret라는 클래스를 추가하면 될 것이다.


 

3. 🛠️input-radio와 label을 이용해 구현

아까 말했던 두 번째 문제인 useState 사용을 배제하고, 아코디언 기능을 구현할 수 있다.

상태가 없다는 것은 아주 효율적이라고 생각한다.

    <li className='item item5' key={id}>
      <input
        className='input'
        type="radio"
        name="accordion"
        id={id}
        defaultChecked={initialChecked}
      />
      <label htmlFor={id} className='tab'>
        {title}
      </label>
      <div className='description'>{description}</div>
    </li>

각각의 아코디언 아이템을 JSX로 위처럼 구현한다.

그리고 아래와 같이 스타일을 주면된다.

일단 styled-component보다 sass가 더 보기 수월할 것 같아서 sass로 올려보았다.

.item5 {
    overflow: hidden;

    .input {
      display: none;
    }
    .tab {
      display: block;
    }
    .description {
      padding: 0 15px;
      border-bottom-width: 0;
      max-height: 0;
      transition: ease 0.3s;
    }
    .input:checked {
      + .tab {
        background-color: #ace;

        &::before {
          content: '-';
        }
      }
      ~ .description {
        padding: 15px;
        border-bottom-width: 1px;
        max-height: 300px;
      }
    }
  }

간단하게 설명하면

  1. input은 일단 안보이게 해야한다 (display : none)
  2. tab은 클릭해서 보여야하는 label 태그 부분이므로 보이게 한다. (display : block)
  3. description 부분은 처음에 max-height: 0을 줘서 안보이게 한다.
  4. input:checked를 활용해서 열렸을 때의 css 스타일을 모두 지정해준다.

transition 효과도 가능해서 이 구현 코드가 나는 굉장히 마음에 들었다.

 

 

4. 🛠️details와 summary를 이용해 아코디언 기능 만들기

<details> 태그와 <summary> 태그는 HTML5에서 새로 도입된 태그로, 사용자가 클릭해서 추가 정보를 볼 수 있도록 하는 인터랙티브한 요소를 만들 때 유용합니다.

여기 링크를 보면 자세하게 나와있다.

<details name="test">
    <summary>요약 내용</summary>
    추가 정보나 세부 내용
</details>

이대로만 작성하면 알아서 클릭시 세부 내용이 나오고 닫히게 됐다.

name으로 연결하면 하나의 details가 열렸을 때 알아서 닫힌다...!

🤔transition 효과는 어떻게 줘야할까?

근데 details, summary 태그를 사용하니 transition이 잘 안됐다..

일단 details는 open 속성에 따라 열고 닫힌다.

근데 open이 없더라도 일단 내부의 내용은 DOM요소에 띄워져있다.

그렇다면 transition은 살짝 다르게 해야한다.

 

details안에는 summary와 summary와 형제요소인 컨텐츠가 있다.

근데 summary를 누르면 details의 open 속성이 추가된다.

 

그니까 이걸 자세히 말하면 details와 summary, 컨텐츠는 원래 계속 보여지는 상태인데

컨텐츠 부분이 어떠한 작용으로 open이 아니면 안보이는 것이다.

overflow: hidden 과 같은 속성으로 안보이는 것이 아니다!

 

그래서 transition효과를 적용하려면 

먼저 details태그에 summary가 보여질 정도의 height 또는 max-heigth를 적용한다.

그럼 open이거나 open이 아닐 때 summary는 일단 보이게 된다.(0을 적용하면 open아닌 것들은 안보입니다)

그리고 deatail[open]이 되면 height 또는 max-heigth를 지정한 높이로 늘려준다.

여기서 transition은 details 태그에 적용해놓으면 된다.

 

말로는 이해가 어려우니까 코드를 보자

  details {
    transition: max-height 1s ease;
    overflow: hidden; -> transition효과가 일어날 때 컨텐츠요소가 먼저 보이지 않게하기위해 적용!
  }

  details:not([open]) {
    padding: 10px;
    height: 50px;  -> 닫혔을 때 고정되지 않는 높이때문에 명시
    max-height: 50px;
  }

  details[open] {
   transition: max-height 3s ease;// 이 부분은 아래 설명
    max-height: 100vh;
  }


  details[open] {
    max-height: 100vh;
  }

  details[open] summary {
    background-color: #ace;
    padding: 15px;
    border-bottom-width: 0;
  }
  
 summary::marker {
    content: '+ ';
  }
  details[open] summary::marker {
    content: '- ';
  }

스택오버플로우 링크에서는 다양한 방법을 제시하고 있었는데 그 중에서 animation을 사용해야 한다고 얘기하는 비중이 많았다.

나는 뭔가 될 것 같은데라고 생각해서 위처럼 만들어봤다.

 

주석처럼 단 부분도 추가해주는 게 좋았다.

그 중에서 details[open] 일 때도 transition을 넣어준 부분이 있는데 

하나의 details 요소만 계속 클릭하면 transition 효과가 나타나지 않았다.

시간이 좀 지나야 transition 효과가 나타나게 되는데(이것이 의문이다..)

그래서 details[open]에도 달아줘야 transition이 일어날 확률이 증가한다(이것도 왜 확률인지가 의문이다...어쩔 때는 transition이 적용되지않는다)

 

📢정리

아래 gif보면 깔끔하긴 하지만 not([open])일 때 hegiht와 max-height를 잘 지정해야한다는 것이 문제인 것 같았다.

그리고 닫힐 때는 transition이 어쩔 때(?) 적용되지 않았다.(시간이 지나면 잘 적용됨)

참고로 연속으로 누르면 transition효과가 안나타난다..(이건 왜그럴까?)

 


 

 

이렇게 4가지로 아코디언 기능을 활용하는 방법을 알아보았다.

 

 

🛠️추가 기능 Ctrl + F로 텍스트 찾으면 아코디언 열리도록 하기

먼저 해당 링크를 참고했다.

위 링크에서 자세히 설명하기 때문에 간단하게 설명하려고 한다.

 

const AccordionItem = ({
...
}) => {
  const descRef = useRef<HTMLDivElement>(null)

  //descRef로 연결한 요소에 beforematch라는 이벤트를 등록해야하는데
  //beforematch는 HTML에서 새로 등장한 기능이다 보니 React가 반영을 못하고 있다.
  //그래서 addEventListener로 등록을 해준 것이다.
  useEffect(() => {
    const node = descRef.current
    if (node) {
      node.addEventListener('beforematch', toggle)
    }
    return () => {
      if (node) node.removeEventListener('beforematch', toggle)
    }
  }, [toggle])

  //원래 HIDDEN이 아닌 hidden으로 해야하는데 
  //React에는 "until-found"라는 것이 아직 반영이 안되어있어서 없애버리는 문제가 있었는데
  //HTML이 대소문자를 구분안하는 꼼수로 until-found를 인식하게 했다.
  
  //또한 현재 styled-component는 HIDDEN속성은 되고있지만 beforematch 이벤트가 작동이 안되고 있다.
  //그래서 description부분은 div 태그를 사용해서 구현했다.
  return (
    <AccordionItemLi
      current={current}
      key={id}>
      <AccordionTab
        current={current}
        onClick={toggle}>
        {title}
      </AccordionTab>
      <div
        className="description"
        ref={descRef}
        HIDDEN={current ? undefined : 'until-found'}> ->여기 부분 포인트!
        {description}
      </div>
    </AccordionItemLi>
  )
}

보기 쉬울지는 모르겠지만 주석으로 달아놓았다.

이렇게 하면 아래처럼 ctrl+F로 찾은 내용이 있다면 아코디언을 자동으로 열어주게 된다.

화질구지..죄송합니당

 

원래 beforematch 이벤트가 뭔지, hidden 속성이 뭔지도 추가하고 싶었는데 링크에서 모두 설명해주고 있기에 간단하게만 설명하겠다.

 

🤔beforematch 이벤트

beforematch 이벤트는 사용자가 페이지 내의 특정 콘텐츠를 찾기 위해 브라우저 검색 기능(예: Ctrl+F)을 사용할 때 발생하는 이벤트입니다.
이 이벤트는 사용자가 검색한 텍스트가 페이지 내의 콘텐츠와 일치하기 직전에 발생합니다.
이 이벤트를 사용하면 숨겨진 콘텐츠를 동적으로 표시하거나, 검색 결과에 맞춰 페이지를 조정할 수 있습니다.

 

🤔HTML hidden

hidden은 원래 " 해당 요소가 아직, 또는 더 이상 관련이 없음을 나타내는 불리언 특성입니다. " 였다.

그래서 요소가 그냥 아예 안보이게 하는 것이었는데 

새로운 속성이 추가되었다.

위 링크에 설명이 되어있는데 영어로 되어있을 것이다.

번역해서 보면 아래와 같았다.

사용방법을 보여주고 있다.

📘마무리하며...

이렇게 아코디언을 만들 수 있는 방법 4가지와 ctrl + F 기능도 알아보았다.

자료를 보면서 내가 추가해보고 싶은 부분은 추가하고, 디테일하게 알고 싶은 부분도 추가해보았다.

이렇게 직접 만들면서 하거나 포스팅을 작성하면서 "왜 이렇게 될까?" 하는 부분들이 종종 있다.

그래서 직접해보는 것이 좋다고 생각했고, 이렇게 포스팅하게 되었다.

 

뭔가 두서없는 부분들이 있는 것 같은데 추후 계속 읽어보면서 정리하려고 한다..ㅠㅠ 체력이슈

아무튼 다음 UI 컴포넌트를 만들 때도 이렇게 할 생각이다.

 

 

728x90
반응형
LIST