🔍 배경 및 궁금증
현재 입력을 할 때마다 태그가 필터링되는 기능을 구현하려고 하는데, 문제가 발생했다.
바로, input을 한 글자씩 입력할 때마다 focus가 out되는 문제가 발생하는 것.
따라서 이를 해결하기 위해 서칭 중이다.

혹시나 몰라서 남기는 Sidebar
code.
import React, { useState, useEffect, useMemo, useCallback, useRef, } from 'react'; import styled from '@emotion/styled'; import Image from '@components/common/Image'; import MoreOptionsContainer from '@components/main/Sidebar/MoreOptionsContainer'; import Modal from '@components/common/Modal'; import { useUserInfo } from '@contexts/UserInfoProvider'; import colors from '@assets/colors'; import useLocalStorage from '@hooks/useLocalStorage'; import { css } from '@emotion/react'; const SidebarContainer = styled.aside` position: fixed; background: #252b2e; width: 500px; height: 100%; color: #fff; `; const HeaderContainer = styled.div` display: flex; `; const SidebarHeader = styled.header` display: flex; align-items: center; justify-content: center; background: #5fb3b3; width: calc(100% - 50px); height: 50px; `; const ToggleButton = styled.button` display: flex; align-items: center; justify-content: center; cursor: pointer; width: 50px; height: 50px; `; const UserListContainer = styled.div` margin: 40px 16px; background: rgba(256, 256, 256, 0.1); padding: 0 16px; height: 500px; `; const UserInfoCount = styled.div` padding: 20px 0; height: 30px; `; const UserList = styled.div` margin: 0; padding: 0; height: 400px; overflow-y: auto; list-style: none; `; const UserCard = styled.div` box-sizing: border-box; display: flex; background: rgba(256, 256, 256, 0.1); padding: 16px; color: #fff; &:not(:last-child) { margin-bottom: 8px; } `; const UserAuthBadge = styled.div` ${({ visible }) => visible ? css` display: block; ` : css` display: none; `} position: absolute; left: -10px; top: -10px; width: 16px; height: 16px; border-radius: 50%; background: ${colors.default.green}; `; const UserInfo = styled.div` margin-left: 16px; `; const Username = styled.div` font-size: 20px; `; const UserDoing = styled.div` font-size: 16px; `; const UserImageContainer = styled.div` position: relative; border-radius: 50px; width: 50px; height: 50px; `; const MyInfoContainer = styled.div` position: absolute; bottom: 0; width: 100%; `; const ShowMoreButton = styled.button` transform: ${({ isActive }) => (isActive ? `rotate(90deg)` : '')}; transition: transform 0.3s; margin-left: auto; width: 60px; height: 50px; `; const ModalUserTime = styled.h2``; const ModalLogoutHeader = styled.h1``; const ModalButtonBox = styled.ul` display: flex; `; const ModalConfirmButton = styled.button` background-color: ${colors.default.green}; `; const ModalCancelButton = styled(ModalConfirmButton)` background-color: ${colors.default.red}; `; const ModalTagDropdown = styled.section` display: flex; flex-direction: column; align-items: center; height: 100%; `; const DropdownHeader = styled.h3` strong { font-weight: 700; } `; const DropdownForm = styled.form` display: block; width: 100%; height: 30px; `; const DropdownInput = styled.input` width: 100%; height: 100%; box-sizing: border-box; `; const TagList = styled.ul` overflow-y: scroll; height: 240px; `; const TagItem = styled.li` display: flex; align-items: center; padding: 0px 4px; width: 100%; height: 36px; font-size: 20px; box-sizing: border-box; &:hover { background-color: ${colors.secondary300}; } `; const TagItemTodo = styled.div` font-weight: bold; `; const TagItemRemoveButton = styled.button` display: none; margin-left: auto; &.active { display: block; } `; const ConfirmButton = styled(ModalConfirmButton)``; const Sidebar = ({ users, isAuth, usersAuth }) => { const { userInfo, emptyUserInfo } = useUserInfo(); const sidebarRef = useRef(null); const placeholder = useMemo( () => 'https://mblogthumb-phinf.pstatic.net/MjAxODEyMDVfMjY5/MDAxNTQ0MDA3NDgyNjgw.v21vfp4yFzGtYlNrFPeo7Cxkd6ZVa3ZNKeRwZe5l3e0g.y2pAI3tJYWq04q_FwbVgTOoTVo9bKcwISdhj9EAxNYgg.GIF.nang723/IMG_0834.GIF?type=w800', [] ); const [isShowMoreButtonActive, setIsShowMoreButtonActive] = useState(false); const [mousePosition, setMousePosition] = useState({ x: 0, y: 0, width: 0, height: 0, }); const handleShowMoreButtonClick = useCallback(({ clientX, clientY }) => { setIsShowMoreButtonActive((state) => !state); setMousePosition(() => ({ x: clientX, y: clientY, width: document.body.clientWidth, height: sidebarRef.current.clientHeight, })); }, []); const [isStepOut, setIsStepOut] = useState(false); const handleStepOutButtonClick = () => { setIsStepOut((state) => !state); setIsShowMoreButtonActive(false); }; const [visible, setVisible] = useState(false); const handleLogoutButtonClick = () => { setVisible(() => true); }; useEffect(() => { if (!visible) { setIsShowMoreButtonActive(() => false); } }, [visible]); const [visibleDropdown, setVisibleDropdown] = useState(false); const handleTagButtonClick = () => { setVisibleDropdown(() => true); }; const inputRef = useRef(null); const [tags, setTags] = useLocalStorage('tags', []); const [nowTag, setNowTag] = useState(''); const [inputTagValue, setInputTagValue] = useState(''); const [filteredTags, setFilteredTags] = useState([]); useEffect(() => { if (inputTagValue) { setFilteredTags(() => tags); } setFilteredTags(() => tags.filter((tag) => tag.includes(inputTagValue))); console.log(filteredTags); }, [inputTagValue]); const handleInput = (e) => { console.log(e.target.value); setInputTagValue(inputRef.current.value); }; const handleNowTagChange = (e) => { e.preventDefault(); if (!inputTagValue) { return; } setTags([...new Set([...tags, inputValue])]); setInputTagValue(() => ''); }; const handleTagItemClick = (e) => { const closestLi = e.target.closest('li'); if (!closestLi) return; if (e.target.classList.contains('tagRemoveButton')) { setTags((state) => state.filter( (content) => content !== closestLi.querySelector('div').textContent ) ); return; } const value = closestLi.querySelector('div').textContent; setInputTagValue(() => ''); setNowTag(value); setVisibleDropdown(() => false); setIsShowMoreButtonActive(() => false); }; const isBadgeVisible = (id) => { return usersAuth.includes(id); }; const handleTagItemHover = (e) => { const closestLi = e.target.closest('li'); if (!closestLi) return; const activeButton = closestLi.querySelector('button'); e.currentTarget.querySelectorAll('li > button').forEach((buttonElement) => { buttonElement.classList.toggle('active', buttonElement === activeButton); }); }; return ( <SidebarContainer ref={sidebarRef}> <HeaderContainer> <SidebarHeader>방 이름</SidebarHeader> <ToggleButton>토글!</ToggleButton> </HeaderContainer> <UserListContainer> <UserInfoCount>함께하는 사람들: {users.length}명</UserInfoCount> <UserList> {users.map(({ image, _id, fullName, updatedAt }) => ( <UserCard key={_id}> <UserImageContainer> <UserAuthBadge visible={isBadgeVisible(_id)} /> <Image src={image} lazy block width={50} height={50} placeholder={placeholder} alt="profile" isCircle /> </UserImageContainer> <UserInfo> <Username>{fullName}</Username> <UserDoing>{updatedAt}</UserDoing> </UserInfo> </UserCard> ))} </UserList> </UserListContainer> <MyInfoContainer> <UserCard> <UserImageContainer> <UserAuthBadge visible={isAuth} /> <Image src={userInfo.image} lazy block width={50} height={50} placeholder={placeholder} alt="profile" isCircle /> </UserImageContainer> <UserInfo> <Username>{userInfo.fullName}</Username> <UserDoing>{isStepOut ? `🚫 자리비움` : nowTag}</UserDoing> </UserInfo> <ShowMoreButton isActive={isShowMoreButtonActive} onClick={handleShowMoreButtonClick} > 미트볼 버튼 </ShowMoreButton> <MoreOptionsContainer visible={isShowMoreButtonActive} mousePosition={mousePosition} onClose={() => !visible && setIsShowMoreButtonActive(false)} > <button type="button" onClick={handleStepOutButtonClick}> {isStepOut ? `자리비움 취소` : `자리비움`} </button> <button type="button" onClick={handleLogoutButtonClick}> 로그아웃 </button> <button type="button" onClick={handleTagButtonClick}> 할 일 수정 </button> </MoreOptionsContainer> <Modal isDimBlack width={400} height={400} visible={visible || visibleDropdown} onClose={() => { if (visible) setVisible(() => false); if (visibleDropdown) setVisibleDropdown(() => false); }} > {visible && ( <> <ModalUserTime>{`현재 ${userInfo.fullName}님이 모각코한 시간: `}</ModalUserTime> <ModalLogoutHeader> 정말로 로그아웃 하시겠어요? </ModalLogoutHeader> <ModalButtonBox> <ModalConfirmButton onClick={emptyUserInfo}> 확인 </ModalConfirmButton> <ModalCancelButton className="modal__cancel-button" onClick={() => { setVisible(() => false); }} > 취소 </ModalCancelButton> </ModalButtonBox> </> )} {visibleDropdown && ( <ModalTagDropdown> <DropdownHeader> 현재 하고 있는 일을 <strong>반.드.시</strong> 입력하세요! </DropdownHeader> <DropdownForm onSubmit={handleNowTagChange}> <DropdownInput ref={inputRef} onInput={handleInput} /> <TagList onClick={handleTagItemClick} onMouseOver={handleTagItemHover} > {/* {!inputTagValue ? tags.map((tag) => ( <TagItem key={tag}> <TagItemTodo>{tag}</TagItemTodo> <TagItemRemoveButton type="button" className="tagRemoveButton" > ❌ </TagItemRemoveButton> </TagItem> )) : tags .filter((content) => content.includes(inputTagValue)) .map((tag) => ( <TagItem key={tag}> <TagItemTodo>{tag}</TagItemTodo> <TagItemRemoveButton type="button" className="tagRemoveButton" > ❌ </TagItemRemoveButton> </TagItem> ))} */} </TagList> </DropdownForm> <ConfirmButton onClick={handleNowTagChange}> 할 일 추가 </ConfirmButton> </ModalTagDropdown> )} </Modal> </UserCard> </MyInfoContainer> </SidebarContainer> ); }; export default Sidebar;
📢 해결 방법
[참고 자료]를 봤을 때, 분리를 한다면 오류가 발생하지 않는다고 한다.
한 번 컴포넌트를 분리해보자.
실제로 컴포넌트를 분리한 후에는 오류가 뜨지 않았다.
아무래도 그 원인은, 리액트 내부에서 원래는
input
에 대해 리렌더링이 될 경우, - 다시 해당 컴포넌트가 같은 컴포넌트임을 인식하고
focus
를 처리해주어야 하는데, 이러한 부분이 제대로 되어 있지 않았던 것이었다.
따라서 다음과 같이 컴포넌트를 분리하며 진행하였다.