이시안 개발 블로그

auto complete 구현하기 본문

🌐Web

auto complete 구현하기

ICAN 2021. 11. 24. 16:12

✨ 왜?

현재 하고있는 프로젝트에서 구현하고 싶었던 기능 중 하나인 auto complete
Spring에서 비동기로 데이터를 받아서 보여주는 것을 해보고 싶었습니다.
사용자 입장에서 현재 검색어가 유효한 것인지 아닌지 정보를 전달하는 것이 필요하지 않을까라는 생각이었습니다.

샘플 데이터라 아직 이미지가 없는 점 양해바랍니다.

Controller

@RequestMapping("/search")
@RestController
public class SearchRestController {

    @Setter(onMethod_ = @Autowired)
    private SearchService service;

    // 검색어 자동완성
    @PostMapping(
            value = "/{word}",
            produces = "application/text; charset=utf8" // 설정하지 않으면 한글이 깨짐
    )
    public String getContainsWord(
            @RequestBody String json
    ) {
        log.debug("getContainsWord() invoked.");
        log.info("json: {}", json);

        // JSON 데이터 변환 Gson
        Gson gson = new Gson();
        SearchWordDTO searchWord = new SearchWordDTO();

        // JSON -> String
        JsonElement element = JsonParser.parseString(json);
        String word = element.getAsJsonObject().get("word").getAsString();

        searchWord.setWord(word);

        List<PartyVO> list = this.service.getContainsWord(searchWord);
        list.forEach(log::info);

        // list -> JSON 변환
        String serializeString = gson.toJson(list);
        log.info("serializeString: {}", serializeString);

        return serializeString;
    } // getContainsWord

} // end class

우선 JSON 데이터를 받기 위해 @RestController 어노테이션을 사용했습니다.
@RestController@Controller@ResponseBody가 추가된 어노테이션으로 데이터를 JSON으로 주고 받을 수 있습니다.
RESTful 웹 서비스를 개발하는 데 쓰이는 기술입니다.

Html

<div class="container-sm p-0">
    <input type="text" name="word" id="search-bar" class="form-control"
    placeholder="찾고 있는 파티를 입력해보세요."
    aria-label="Search">
    <button class="search-btn">
        <span>
            <i class="fas fa-search"></i>
        </span>
    </button>
</div>
<div class="search-box hide">
    <ul class="search-ul p-0"></ul>
</div>

뷰에서는 검색창과 검색 결과를 담아줄 ul을 만들어 줬습니다.
자바스크립트를 이용해서 검색 결과를 li에 담아줄 것입니다.

JavaScript

const searchBar = document.querySelector("#search-bar");
const searchBox = document.querySelector(".search-box");
const searchUl = document.querySelector(".search-ul");
let cache = ""; // url 값을 담을 변수

우선 선택자를 이용해서 필요한 요소를 변수로 지정했습니다.

// 타이머
const timer = (beforeInput) => {
    // 0.5초마다 검색어를 확인
    // 하나하나 변경될때마다 데이터를 넘기는 것보다 딜레이가 있는 것이 낫다 판단
    setTimeout(() => {
        if (searchBar.value === beforeInput) {
            loadData(searchBar.value);
            checkInput();
        } else {
            checkInput();
        } // if-else

        // 검색어가 없으면 요소를 숨김
        if (searchBar.value !== "") {
            searchBox.classList.remove("hide");
        } else {
            searchBox.classList.add("hide");
        } // if-else
    }, 500);
} // timer

// 일정 시간 간격으로 조회
const checkInput = () => {
    const beforeInput = searchBar.value;
    timer(beforeInput);
} // checkInput

검색창의 데이터를 input의 값이 바뀔 때마다 가져온다면 서버쪽과의 통신이 너무 자주 일어나게 됩니다.
때문에 입력값이 바뀌더라도 0.5초에 한번 씩 실행하도록 함수를 만들어 사용했습니다.

// 검색어 불러오기
const loadData = (word) => {
    let url = `/search/${word}`;

    // 검색어를 입력하면 url 값이 변경된다
    if (cache !== url) {
        cache = url;
        let data = {};

        data.word = word;
        console.log(data); // {word = "축구"}

        fetch(cache,
            {
                method: "POST",
                headers: {
                    "Content-Type": "application/json;",
                },
                body: JSON.stringify(data),
            }
        )
            // response 객체를 json 변환
            .then((res) => res.json())
            .then((data) => {
                console.log(data);
                fillSearch(data); // data로 list 만드는 함수 실행
            })
            .catch((err) => {
                console.log(err);
            });
    }
} // loadData

데이터를 주고 받는 비동기 통신수단으로 fetch api를 사용했습니다. $.ajax()xhr api를 사용해도 똑같이 구현할 수 있지만
fetch api는 데이터 전송을 하고나서 Promise 객체를 넘겨주며 성공했거나 실패했을 시에 대한 처리가 쉽습니다.
데이터를 받는 데 성공했다면 .then()을 실행시키고 받은 데이터를 가공하도록 처리합니다.

// 검색어 자동완성 리스트 채우기
const fillSearch = (data) => {
    searchUl.innerHTML = "";

    // 데이터 가공하기
    data.forEach((el, idx) => {

        // html 요소 생성
        const li = document.createElement("li");
        const leftBox = document.createElement("div");
        const thumbBox = document.createElement("div");
        const thumbnail = document.createElement("img");
        const party = document.createElement("span");

        const rightBox = document.createElement("div");
        const hobby = document.createElement("span");
        const local = document.createElement("span");
        const link = document.createElement("a");

        // 요소에 필요한 class, 속성 정의하기
        leftBox.classList.add("left-box");
        thumbBox.classList.add("thumb-box");
        party.classList.add("mx-3");
        thumbnail.setAttribute("src", el.logoPic);
        party.innerHTML = el.partyName;
        thumbBox.appendChild(thumbnail);
        leftBox.appendChild(thumbBox);
        leftBox.appendChild(party);

        rightBox.classList.add("right-box");
        badgeClass(hobby);
        badgeClass(local);
        rightBox.appendChild(hobby);
        rightBox.appendChild(local);
        hobby.innerHTML = el.hobbyName;
        local.innerHTML = el.localName;

        link.setAttribute("href", `/`);

        link.appendChild(leftBox);
        link.appendChild(rightBox);
        li.appendChild(link);
        searchUl.appendChild(li);
    })
} // fillSearch

비동기로 받은 데이터로 html 요소를 자바스크립트에서 만들어서 뿌려주는 함수입니다.

☄️ 결과

샘플 데이터라 아직 이미지가 없는 점 양해바랍니다.

개선사항

- 다른 서비스 사이트들처럼 입력값이 있을 때만 리스트를 생성하기

- 검색창이 아닌 다른 부분을 클릭하면 리스트를 숨기고 다시 활성화하는 토글 기능

- 리스트를 생성하는 다른 방식의 여부?

백과 프론트 간 데이터를 주고 받는 것을 살짝 맛만 보았습니다.

참고: [https://velog.io/@goody/%EA%B2%80%EC%83%89%EC%96%B4-%EC%9E%90%EB%8F%99%EC%99%84%EC%84%B1]

Comments