J2ong 님의 블로그

Spring + React 한글 초성 검색 기능 구현하기 본문

개인 프로젝트

Spring + React 한글 초성 검색 기능 구현하기

J2ong 2025. 4. 26. 14:23

안녕하세요!

이번 글에서는 Spring + React 기반의 프로젝트에서 한글 초성 검색이 가능한 사용자 검색 기능을 어떻게 구현했는지 정리해보겠습니다.

기본적인 방식은 사용자의 이름에서 초성만을 추출하여 별도로 저장하고, 검색 시 입력된 검색어를 초성으로 변환하여 비교하는 방식입니다. 이를 통해 'ㅎㄱㄷ'과 같이 초성만 입력해도 '홍길동'과 같은 이름이 검색될 수 있습니다.


📌  검색 기능 구현 (DB)

컬럼명 설명
user_name 사용자의 이름
user_consonants 이름에서 추출한 초성 문자열

 


 

📌 검색 기능 구현 (Spring)

Handler (초성 추출 유틸 클래스)
public class KoreanUtils {
        private static final char HANGUL_BEGIN_UNICODE = 44032; // 가
        private static final char HANGUL_LAST_UNICODE = 55203;  // 힣
        private static final char HANGUL_BASE_UNIT = 588;
        private static final char[] INITIAL_CONSONANTS = {'ㄱ', 'ㄲ', 'ㄴ', 'ㄷ', 'ㄸ', 'ㄹ', 'ㅁ', 'ㅂ', 'ㅃ',                                                      'ㅅ', 'ㅆ', 'ㅇ', 'ㅈ', 'ㅉ', 'ㅊ', 'ㅋ', 'ㅌ', 'ㅍ', 'ㅎ'};
    
        public static String extractInitialConsonants(String text) {
            if (text == null) return "";
            
            StringBuilder result = new StringBuilder();
            for (char ch : text.toCharArray()) {
                if (ch >= HANGUL_BEGIN_UNICODE && ch <= HANGUL_LAST_UNICODE) {
                    int initialIndex = (ch - HANGUL_BEGIN_UNICODE) / HANGUL_BASE_UNIT;
                    result.append(INITIAL_CONSONANTS[initialIndex]);
                } else {
                    result.append(ch);
                }
            }
            return result.toString();
        }
}

 

Entity
@Entity
public class UserEntity {

    @Id
    private String userName;

    private String userConsonants;

    @PrePersist
    @PreUpdate
    private void updateConsonants() {
        this.userConsonants = KoreanUtils.extractInitialConsonants(this.userName);
    }
}

 

Repository
@Query(
    value =
        "SELECT u.* " +
        "FROM user u " +
        "WHERE " +
        "    LOWER(u.user_name) LIKE LOWER(CONCAT('%', :searchWord, '%')) " +
        "    OR u.user_consonants LIKE CONCAT('%', :consonantSearch, '%')",
    nativeQuery = true
)
List<GetUserListResultSet> searchUsers(
    @Param("searchWord") String searchWord,
    @Param("consonantSearch") String consonantSearch
);

 

Service
String consonantSearch = KoreanUtils.extractInitialConsonants(searchWord);
List<GetUserListResultSet> userList = userRepository.searchUsers(searchWord, consonantSearch);

 

Controller
@GetMapping("/{searchWord}/search-list")
public ResponseEntity<? super GetUserSearchListResponseDto> getSearchUserList(
    @PathVariable("searchWord") String searchWord
) {
    ResponseEntity<? super GetUserSearchListResponseDto> response = userService.getSearchUserList(searchWord);
    return response;
}

 


 

📌 검색 기능 구현 (React)

API 요청 (Axios)
const GET_SEARCH_USER_LIST_URL = (searchWord: string) => `${API_DOMAIN}/user/${searchWord}/search-list`;
export const getSearchUserListRequest = async ( searchWord: string) => {
    const result = await axios.get(GET_SEARCH_USER_LIST_URL(searchWord))
        .then(response => {
            const responseBody: GetSearchUserListResponseDto = response.data;
            return responseBody;
        })
        .catch(error => {
            if (!error.response) return null;
            const responseBody: ResponseDto = error.response.data;
            return responseBody;
        })
    return result;
}

 

컴포넌트 (자동완성 포함)
const SearchButton = () => {
    const [word, setWord] = useState('');
    const [autoCompleteResults, setAutoCompleteResults] = useState<GetSearchUserListResponseDto | null>(null);
    const searchInputRef = useRef<HTMLInputElement | null>(null);

    // 디바운스 처리
    const useDebounce = (value: string, delay: number) => {
        const [debouncedValue, setDebouncedValue] = useState(value);

        useEffect(() => {
            const handler = setTimeout(() => setDebouncedValue(value), delay);
            return () => clearTimeout(handler);
        }, [value, delay]);

        return debouncedValue;
    };

    const debouncedWord = useDebounce(word, 300);

    // 검색어 변화 감지
    const onSearchWordChangeHandler = (e: ChangeEvent<HTMLInputElement>) => {
        setWord(e.target.value);
    };

    // 자동완성 실행
    useEffect(() => {
        const fetchResults = async () => {
            if (debouncedWord.length > 0) {
                const results = await getSearchUserListRequest(debouncedWord);
                if (results && 'userList' in results) setAutoCompleteResults(results);
                else setAutoCompleteResults(null);
            } else {
                setAutoCompleteResults(null);
            }
        };
        fetchResults();
    }, [debouncedWord]);

    return (
        <div className="header-search-container">
            <div className="header-search-input-box">
                <input
                    ref={searchInputRef}
                    className="header-search-input"
                    type="text"
                    placeholder="검색어를 입력해주세요."
                    value={word}
                    onChange={onSearchWordChangeHandler}
                />
                <div className="icon-button">
                    <span className="material-symbols-outlined">search</span>
                </div>
            </div>

            {autoCompleteResults?.userList?.length > 0 && (
                <ul className="autocomplete-results">
                    {autoCompleteResults.userList.map((user, idx) => (
                        <li key={idx}>
                            <div className="results-list-box">
                                <div className="results-list-user-info">
                                    <div className="results-list-profile-box">
                                        <img className="results-list-profile-image" src={user.profileImage} />
                                    </div>
                                    <div className="results-list-box-content">
                                        <div className="user-name-list">{user.userName}</div>
                                    </div>
                                </div>
                            </div>
                        </li>
                    ))}
                </ul>
            )}
        </div>
    );
};

 


 

이처럼 한글 초성 검색 기능은 단순한 LIKE 검색이 아닌, 초성 추출 로직초성 컬럼 저장, Spring + React 연동을 통해 손쉽게 구현할 수 있습니다. 사용자 입장에서는 ‘ㅎㄱㄷ’ 만 입력해도 ‘홍길동’ 사용자를 찾을 수 있는 편리한 UX를 제공할 수 있습니다.

오늘도 즐거운 개발 되시길 바랍니다!