Ajax를 이용한 places search 기능 구현하기

javascript30 6번째 강의인 ‘ajax type ahead’는 ajax를 통해 도시 정보를 로드하고, 사용자의 input text에 해당하는 도시 정보를 보여준다.

이번에도 역시 es6만으로 구현된 해당 강의를 react와 redux를 사용해 수정해보았다.

// actions/index.jsexport const LOAD_DATA = 'LOAD_DATA';
export const CHANGE_INPUT = 'CHANGE_INPUT';

export function loadData(data) {
return {
type: LOAD_DATA,
data: data
}
}

export function changeInput(text) {
return {
type: CHANGE_INPUT,
text: text
}
}

action은 처음 페이지가 로드되었을 때 도시 정보를 가져오는 LOAD_DATA action과 사용자의 input내용에 따라 해당 도시를 보여주는 CHANGE_INPUT 두 가지가 있다.

components/inputForm.js

fetch() function을 이용해 받은 데이터로 loadData action을 만들어 store에 넘겨주고, input tag에는 onChange() function을 등록해주었다.

cityName과 stateName은 input 창에 입력한 해당 키워드를 강조표시해준다.

regExp는 정규표현식을 사용하기 위한 객체로, ‘gi’중 ‘g’는 텍스트 전체에서 일치하는 문자를 찾을 때, ‘i’는 대소문자를 구별하지 않을 때 사용하는 옵션이다.

즉, ‘문자열 전(city or state)중에 {input}내용과 일치하는 부분은 (<span class=”hl”>{input}</span>)으로 대체'한다"라는 의미가 된다.

눈여겨 볼 점은 dangerouslySetInnerHTML이다. react에서는 XSS공격을 막기 위해서 render()안에서는 html tag가 단순 string으로 렌더링 되기 때문에 이를 억지로 html tag로 렌더링하기 위해서 다음과 같이 표현해주었다. 권장하는 방법은 아니라고 하지만 별도의 솔루션을 찾지 못해 이렇게라도 처리해주었다.

suggestions의 자세한 로직은 reudx에서 설명하도록 하겠다.

// containers/InputFormContainer.jsimport { connect } from 'react-redux';

import InputForm from '../components/InputForm';
import { loadData, changeInput } from '../actions/';
import { defaultState } from "../reducers";

const mapStateToProps = state => {
if(!state) return defaultState;
return {
...state
}
};

const mapDispatchToProps = dispatch => {
return {
loadData: (data) => {
dispatch(loadData(data));
},
onChange: (text) => {
dispatch(changeInput(text));
}
}
};

const InputFormContainer = connect(mapStateToProps, mapDispatchToProps)(InputForm);

export default InputFormContainer;

container에서는 mapStateToProps로 state정보를 넘겨주었고 mapDispatchToProps에서 loadData()와 onChange()를 정의해 넘겨주었다.

//reducers/index.jsimport { LOAD_DATA, CHANGE_INPUT } from '../actions/'

export const defaultState = {
input: '',
locations: [],
suggestions: []
};

function places(state = defaultState, action) {
switch(action.type) {
case LOAD_DATA:
return {
...state,
locations: action.data
};
case CHANGE_INPUT:
const matched = state.locations.filter(location => {
const regex = new RegExp(action.text, 'gi');
return location.city.match(regex) || location.state.match(regex);
}).splice(0, 15);

return {
...state,
input: action.text,
suggestions: matched
};
}
}

export default places;

reducer에서는 LOAD_DATA action이 들어왔을 경우 locations state를 갱신해준다. InputForm component가 mount되었을 때 mounted() function에서 단 한 번 사용한다.

CHANGE_INPUT은 input text를 포함한 locations만을 필터링해서 suggestions state에 넣어준다. splice()는 필터링 결과 중 index 0번 부터 15개만 잘라내주는 함수로 생략 가능하다.

Image for post
Image for post

구현 결과는 다음과 같이 input 창에 text를 입력하면 해당 text가 포함 된 도시들을 보여주고, text와 일치하는 부분을 강조해준다.

Github Source Code

woohyun

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store