2주간 빠르게 진행되었던 카카오프렌즈샵 클론 코딩 프로젝트, 리팩토링 할 정신도 없이 기능구현에 몰두했다. 프로젝트 과정에서 나와 다른 팀원들이 작성한 꼭 기억하고 싶은 코드를 정리해보자!
Nav 창에서 검색창에 글자를 입력시, 해당 글자가 포함된 아이템 리스트 8개가 하단에 등장하며, 검색어만 색상이 핑크색으로 변하는 것을 확인할 수 있다. 이를 구현하기 위해서 전체 아이템 리스트에서 8개만을 리스트로 보여주는 것은 slice()
메소드로 가능했다.
그런데 검색어를 span 태그로 묶어서 색을 바꾸려고 하자 색상은 적용이 되지 않을 뿐더러 ...<span>검색어</span>...
이렇게 HTML 태그까지 보이는 문제가 생겼다. 이를 이름부터 무시무시한 dangerouslySetInnerHTML
을 사용해서 해결할 수 있었다. 이 내용은 하단에 나온다!
componentDidMount()
componentDidMount() {
fetch(`${URL}products`)
.then((res) => res.json())
.then((res) => {
const result = res.data_list;
this.setState({
products: result,
});
});
}
컴포넌트가 마운트 될 때, 전체 프로덕트 리스트를 products
state에 추가한다. 매칭된 프로덕트의 이름을 노출시키기 위함이다.
checkMatch()
checkMatch = (e) => {
const searchValue = e.target.value;
const { products } = this.state;
this.setState({
searchValue: searchValue,
matchArr: searchValue
? products.filter((product) => product.name.includes(searchValue))
: [],
});
};
...
<input
onChange={this.checkMatch}
className="SearchbarInput"
type="search"
placeholder="무엇을 찾으세요?"
/>
input 창에 onChange event 발생시 checkMatch()
가 실행된다. 위 함수에서는 input창의 value 값을 계속 state에 세팅하고, searchValue
즉 input 창의 value 가 존재할 경우, 이름에 해당 값이 포함된 리스트를 matchArr
state에 세팅한다.
dangerouslySetInnerHTML
<ul className={`searchResultWrap ${matchArr.length ? "isShown" : ""}`}>
{matchArr.slice(0, 8).map((product) => (
<li
key={product.product_id}
dangerouslySetInnerHTML={{
__html: product.name.replace(
`${searchValue}`,
`<span>${searchValue}</span>`
),
}}
/>
))}
</ul>
matchArr
state 가 새로 set 될때마다 render가 새로 일어나게 된다. matchArr 안에 요소가 있을 경우에 ul 창의 display
가 block
으로 나타나게 되고, matchArr
의 인덱스 0
부터 8개의 요소가 li
태그로 mapping 된다. 상품의 이름에서 searchValue 값은 <span>
태그 안에 들어간 형태로 replace 된다. 브라우저의 DOM 에서 innerHTML
을 사용하기 위한 대체 방법이라고 생각하면 된다.
사이트간 스크립팅 공격에 쉽게 노출될 수 있기 때문에 이를 상기시키기 위해 저렇게 무시무시한 이름을 갖게 되었다고 한다.. ㅋㅋㅋ __html
키로 객체를 전달해야 한다.
dangerouslySetInnerHTML 사용 예시
function createMarkup() {
return {__html: 'First · Second'};
}
function MyComponent() {
return <div dangerouslySetInnerHTML={createMarkup()} />;
}
searchItems()
searchItems = (e) => {
e.preventDefault();
this.setState({
matchArr: [],
});
this.props.history.push("/search", this.state.searchValue);
};
...
<form className="Searchbar" onSubmit={this.searchItems}>
검색창에서 enter 키를 누를 경우 /search
라우트의 컴포넌트 SearchResult로 이동한다. history.push
의 두번째 인자로 searchValue
를 전달한다. push 할때 path 뒤에 state 까지 줄 수 있다는 것도 이번에 알게 되었다!! push(path, [state])
이렇게 검색어까지 검색결과 페이지 컴포넌트에 전달하면 거기서 리스트를 새로 뽑아줄 수 있게 된다!
Searchbar.js
class Searchbar extends React.Component {
constructor() {
super();
this.state = {
products: [],
matchArr: [],
matchShow: [],
searchValue: "",
};
}
componentDidMount() {
fetch(`${URL}products`)
.then((res) => res.json())
.then((res) => {
const result = res.data_list;
this.setState({
products: result,
});
});
}
searchItems = (e) => {
e.preventDefault();
this.setState({
matchArr: [],
});
this.props.history.push("/search", this.state.searchValue);
};
checkMatch = (e) => {
const searchValue = e.target.value;
const { products } = this.state;
this.setState({
searchValue: searchValue,
matchArr: searchValue
? products.filter((product) => product.name.includes(searchValue))
: [],
});
};
render() {
const { matchArr, searchValue } = this.state;
return (
<form className="Searchbar" onSubmit={this.searchItems}>
<div className="box">
<div className="btn">
<span className="icon"></span>
</div>
<input
onChange={this.checkMatch}
className="SearchbarInput"
type="search"
placeholder="무엇을 찾으세요?"
/>
</div>
<ul className={`searchResultWrap ${matchArr.length ? "isShown" : ""}`}>
{matchArr.slice(0, 8).map((product) => (
<li
key={product.product_id}
dangerouslySetInnerHTML={{
__html: product.name.replace(
`${searchValue}`,
`<span>${searchValue}</span>`
),
}}
/>
))}
</ul>
</form>
);
}
}
export default withRouter(Searchbar);
componentDidMount()
showResult = () => {
let keyword = this.props.history.location.state;
this.setState({
searchValue: keyword,
});
fetch(URL + `products?name=${keyword}`, {
method: "GET",
})
.then((res) => res.json())
.then((res) => {
if (res.data_list.length) {
this.setState({
productList: res.data_list,
totalCount: res.data_list[0].total_count,
});
} else {
this.setState({
productList: [],
totalCount: "",
});
}
});
};
componentDidMount() {
this.showResult();
}
this.props.history.location.state
즉 Searchbar.js 에서 넘어온 keyword를 현재 컴포넌트의 state로 설정한다. 쿼리스트링으로 백엔드에서 해당 키워드가 포함된 이름의 제품 리스트를 받아오고, 이를 List에 setState, 총 개수도 setState한다. 만일 검색 결과가 없을 경우, 빈 배열과 스트링으로 세팅한다.
{this.state.productList.length ? (
<>
<div className="topInfo">
<div className="totalItems">
<p className="totalNum">
총 <span className="pointSpan">{totalCount}</span> 개의
상품이 조회되었습니다.
</p>
</div>
<Sorting />
</div>
<List productList={productList} />
</>
) : ( ... )}
productList에 제품리스트가 있는 경우 List
컴포넌트에 해당 리스트를 전달하여 노출시킨다. 제품 리스트가 빈 배열인 경우에는 그에 맞는 이미지와 내용을 보여준다. 아래와 같다.
<h1
dangerouslySetInnerHTML={{
__html: `<span>‘${this.props.history.location.state}’</span> 검색결과`,
}}
/>
여기에서도 dangerouslySetInnerHTML
을 사용했다!
componentDidUpdate()
componentDidUpdate(prevProps) {
if (prevProps.history.location.state !== this.state.searchValue) {
this.showResult();
}
}
검색결과 페이지에 처음 결과를 받아오고 나서 다시 nav 에 내용을 입력하면 하단 결과 리스트가 변하지 않는 문제가 있었는데 componentDidUpdate
로 해결했다. CDU은 적절한 조건을 걸어주지 않으면 무한루프 될 위험이 있으니 주의하자!
SearchResult.js
class SearchResult extends React.Component {
constructor() {
super();
this.state = {
searchValue: "",
productList: [],
totalCount: "",
};
}
showResult = () => {
let keyword = this.props.history.location.state;
this.setState({
searchValue: keyword,
});
fetch(URL + `products?name=${keyword}`, {
method: "GET",
})
.then((res) => res.json())
.then((res) => {
if (res.data_list.length) {
this.setState({
productList: res.data_list,
totalCount: res.data_list[0].total_count,
});
} else {
this.setState({
productList: [],
totalCount: "",
});
}
});
};
componentDidMount() {
this.showResult();
}
componentDidUpdate(prevProps) {
if (prevProps.history.location.state !== this.state.searchValue) {
this.showResult();
}
}
render() {
const { productList, totalCount } = this.state;
return (
<>
<div className="SearchResult">
<Nav />
<div className="resultKeywords">
<h1
dangerouslySetInnerHTML={{
__html: `<span>‘${this.props.history.location.state}’</span> 검색결과`,
}}
/>
</div>
{this.state.productList.length ? (
<>
<div className="topInfo">
<div className="totalItems">
<p className="totalNum">
총 <span className="pointSpan">{totalCount}</span> 개의
상품이 조회되었습니다.
</p>
</div>
<Sorting />
</div>
<List productList={productList} />
</>
) : (
<div className="notFound">
<img
alt="혼란스러운 지형"
className="ryanConfused"
src="https://t1.kakaocdn.net/friends/new_store/2.0/common/img_empty_ryan.png"
/>
<h3>검색결과가 없습니다</h3>
<p>
다른 검색어를 입력하시거나,
<br /> 철자 및 띄어쓰기를 확인해주세요.
</p>
</div>
)}
</div>
<Footer />
</>
);
}
}
export default SearchResult;