카카오클론, 기억하고픈 코드 1 - 검색

meow·2020년 10월 1일
8

Project

목록 보기
3/9
post-thumbnail

2주간 빠르게 진행되었던 카카오프렌즈샵 클론 코딩 프로젝트, 리팩토링 할 정신도 없이 기능구현에 몰두했다. 프로젝트 과정에서 나와 다른 팀원들이 작성한 꼭 기억하고 싶은 코드를 정리해보자!

1. 검색 모달창

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 창의 displayblock으로 나타나게 되고, matchArr의 인덱스 0부터 8개의 요소가 li 태그로 mapping 된다. 상품의 이름에서 searchValue 값은 <span> 태그 안에 들어간 형태로 replace 된다. 브라우저의 DOM 에서 innerHTML을 사용하기 위한 대체 방법이라고 생각하면 된다.

사이트간 스크립팅 공격에 쉽게 노출될 수 있기 때문에 이를 상기시키기 위해 저렇게 무시무시한 이름을 갖게 되었다고 한다.. ㅋㅋㅋ __html 키로 객체를 전달해야 한다.

dangerouslySetInnerHTML 사용 예시

function createMarkup() {
  return {__html: 'First &middot; 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]) 이렇게 검색어까지 검색결과 페이지 컴포넌트에 전달하면 거기서 리스트를 새로 뽑아줄 수 있게 된다!

참고자료 : https://reactrouter.com/web/api/history

전체 코드 : 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);

2. 검색결과 페이지

코드 분석

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한다. 만일 검색 결과가 없을 경우, 빈 배열과 스트링으로 세팅한다.

jsx

{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;
profile
🌙`、、`ヽ`ヽ`、、ヽヽ、`、ヽ`ヽ`ヽヽ` ヽ`、`ヽ`、ヽ``、ヽ`ヽ`、ヽヽ`ヽ、ヽ `ヽ、ヽヽ`ヽ`、``ヽ`ヽ、ヽ、ヽ`ヽ`ヽ 、ヽ`ヽ`ヽ、ヽ、ヽ`ヽ`ヽ 、ヽ、ヽ、ヽ``、ヽ`、ヽヽ 🚶‍♀ ヽ``ヽ``、ヽ`、

0개의 댓글