내가 개발 중인 지도에는 검색창이 포함되어 있다. 사용자가 이 검색창에 특정 지역을 입력하면, 그 지역에 해당하는 상세 지역 목록이 드롭다운 형식으로 화면에 나타나게 되고, 사용자가 드롭다운 목록 중 하나를 선택하면, 선택된 상세 지역으로 지도가 이동하고 드롭다운은 사라지는 기능을 구현하고 있었다.
코드를 살펴 보도록 하자.
MapHeader
import { Box, Flex, Input, Show, Text } from '@chakra-ui/react';
import { ChangeEvent, Dispatch, SetStateAction, useState } from 'react';
import MobileMapHeader from './MobileMapHeader';
import SearchRegionsList from './SearchRegionsList';
import useDebounce from '@/hooks/useDebounce';
import { useGetSearchRegions } from '@/services/regions/query';
import useSearchRegionsInputValue from '@/stores/useSearchRegionsInputValueStore';
interface MapHeaderProps {
map: naver.maps.Map | null;
mapHeaderOptionsArray: string[];
headerOption: string;
setHeaderOption: Dispatch<SetStateAction<string>>;
}
const MapHeader = ({
map,
mapHeaderOptionsArray,
headerOption,
setHeaderOption,
}: MapHeaderProps) => {
// ...생략
// input(검색창) focus 상태 관리
const [isInputFocused, setIsInputFocused] = useState(false);
// zustand를 이용한 input value 전역 상태 관리
const { searchRegionsInputValue, setSearchRegionsInputValue } =
useSearchRegionsInputValue();
// 디바운스
const debouncedValue = useDebounce(searchRegionsInputValue, 500);
// React-Query를 이용한 지역 정보 가져오기
const { data } = useGetSearchRegions(debouncedValue);
// 지역 정보
const regions: SearchRegions[] = data?.locationSearchResponses;
return (
// ...생략
<Box pos="relative" w="330px" h="100%">
<Input
variant="unstyled"
bgColor="gray.50"
border="none"
h="100%"
padding="0 12px"
fontSize="14px"
placeholder="지역명 검색"
value={searchRegionsInputValue}
onChange={(e: ChangeEvent<HTMLInputElement>) =>
setSearchRegionsInputValue(e.target.value)
}
// focus 될 시 isInputFocused상태 true
onFocus={() => setIsInputFocused(true)}
// focus 될 시 isInputFocused상태 false
onBlur={() => setIsInputFocused(false)}
/>
// isInputFocused가 true일 때만 렌더링
{isInputFocused && (
<SearchRegionsList
map={map}
regions={regions}
isInputFocused={isInputFocused}
/>
)}
</Box>
</Flex>
</Box>
// ...생략
</>
);
};
export default MapHeader;
SearchRegionsList
import { Box } from '@chakra-ui/react';
import SearchRegionsListText from './SearchRegionsListText';
interface SearchRegionListProps {
map: naver.maps.Map | null;
regions: SearchRegions[];
isInputFocused: boolean;
}
const SearchRegionsList = ({
map,
regions,
isInputFocused,
}: SearchRegionListProps) => {
return (
<Box
pos="absolute"
top="40px"
zIndex="1"
bgColor="white"
w="100%"
maxH="220px"
overflow="auto"
borderRadius="11px"
>
{isInputFocused && regions?.length === 0 && (
<SearchRegionsListText hasRegions={false}>
검색 결과가 없습니다. 정확한 검색어를 입력해주세요.
</SearchRegionsListText>
)}
// 드롭다운 리스트들
{regions?.map((region, i) => (
<SearchRegionsListText
region={region}
hasRegions={true}
key={i}
map={map}
>
{region.position}
</SearchRegionsListText>
))}
</Box>
);
};
export default SearchRegionsList;
SearchRegionsListText
import { Text } from '@chakra-ui/react';
import { Dispatch, PropsWithChildren, SetStateAction } from 'react';
import { useNavermaps } from 'react-naver-maps';
import useSearchRegionsInputValue from '@/stores/useSearchRegionsInputValueStore';
interface SearchRegionsListTextProps {
region?: SearchRegions;
map?: naver.maps.Map | null;
hasRegions: boolean;
}
const SearchRegionsListText = ({
region,
map,
hasRegions,
children,
setIsInputFocused,
}: PropsWithChildren<SearchRegionsListTextProps>) => {
const navermaps = useNavermaps();
const { setSearchRegionsInputValue } = useSearchRegionsInputValue();
// 드롭다운의 리스트 클릭 시 호출
const handleClickList = () => {
// handleClickList함수의 호출 여부를 판단하는 콘솔
console.log('click!');
if (hasRegions && region && map) {
// 지도 zoom
map.setZoom(16);
// 지도 이동
map.panTo(new navermaps.LatLng(region.latitude, region.longitude));
setSearchRegionsInputValue(region.position);
}
};
return (
<Text
h="45px"
display="flex"
paddingLeft="15px"
alignItems="center"
borderBottom="1px solid"
borderBottomColor="gray.100"
fontSize="13px"
fontWeight="medium"
cursor="pointer"
color={hasRegions ? 'black' : 'gray.400'}
_hover={{
bgColor: 'gray.50',
}}
overflow="hidden"
// 드롭다운 아이템 클릭
onClick={handleClickList}
>
{children}
</Text>
);
};
export default SearchRegionsListText;
사용자가 지도상의 지역을 선택하는 UI 컴포넌트를 클릭했을 때 지도 이동이나 지역 정보 업데이트가 예상대로 진행되지 않는 문제가 발생했다. "click"을 호출하는 콘솔 자체도 출력이 되지 않았던 것으로 보았을 때, handleClickList 함수가 호출되지 않은 것으로 추정되었다.
이 문제의 원인은 SearchRegionsListText 컴포넌트(혹은 Input 외부)가 클릭될 때 Input 컴포넌트의 focus가 해제되어 onBlur 이벤트가 발생하고 isInputFocused 상태가 false로 업데이트 되면서 SearchRegionsList가 언마운트되었기 때문이다. 결과적으로 SearchRegionsListText 내부의 handleClickList 함수가 호출되지 못했다.
이 문제를 해결하기 위해 (setIsInputFocused(false))를 250밀리초 지연시키는 방법을 적용했다. 이 지연은 onClick 이벤트가 처리될 충분한 시간을 제공하여, 사용자의 클릭 동작이 완전히 처리된 후 focus 상실이 이루어지도록 보장했다.
MapHeader
<Input
variant="unstyled"
bgColor="gray.50"
border="none"
h="100%"
padding="0 12px"
fontSize="14px"
placeholder="지역명 검색"
value={searchRegionsInputValue}
onChange={(e: ChangeEvent<HTMLInputElement>) =>
setSearchRegionsInputValue(e.target.value)
}
onFocus={() => setIsInputFocused(true)}
onBlur={() => {
// isInputFocused 상태 변경 지연
setTimeout(() => {
setIsInputFocused(false);
}, 250);
}}
/>
React에서 복잡한 사용자 인터페이스를 다룰 때, 이벤트 처리 순서는 중요하다. 특히 여러 이벤트가 동시에 발생할 가능성이 있는 상황에서는, 이벤트의 처리 순서를 제어하기 위한 추가적인 기술적 고려가 필요하다는 것을 깨달았다. 이번 사례에서 본 지연 기법은 이벤트 간의 충돌을 방지하고, 사용자 인터랙션을 원활하게 처리하는 데 꽤나 효과적인 해결책임을 보여준다. 이러한 접근 방식은 다른 많은 상황에서도 유용하게 활용될 수 있을 것 같다고 생각한다.