리엑트 JS로 만들기 - 검색폼 구현

조 은길·2022년 2월 10일
0

React

목록 보기
3/12
post-thumbnail

검색폼 구현하기

검색 상품명 입력 폼이 위치한다.

1. element의 루트 노드

const element = (
  // <div className="root-container">
  <>
    <header>
      <h2 className="container">검색</h2>
    </header>
    {/* // TODO */}

    <div className="container">
      <form>
        <input type="text" placeholder="검색어를 입력하세요" autoFocus />
        <button type="reset" className="btn-reset"></button>
      </form>
    </div>
    {/* </div> */}
  </>
);

ReactDOM.render(element, document.querySelector("#app"));

리엑트 element는 일반 DOM 엘리먼트처럼 루트 노드가 하나있어야 한다.
즉!! 모든 것을 감싸주는 하나의 부모 엘리먼트가 필요하다.
그러나, 루트 노드가 코드에 반영되는게 싫다면, <> </> 로 처리해줘서 실제 콘솔에는 반영되지 않게 해줄 수도 있다.

원래라면, <input>에 텍스트를 입력하면, 입력한 값이 출력되서 나올 것이다.


2. 리엑트 컴포넌트

위에 코드를 리엑트 컴포넌트로 만들어보자!!


// TODO
class App extends React.Component {
  render() {
    // render()는 리엑트 엘리먼트를 반환해야 한다.
    return (
      <>
        <header>
          <h2 className="container">검색</h2>
        </header>
        <div className="container">
          <form>
            <input type="text" placeholder="검색어를 입력하세요" autoFocus />
            <button type="reset" className="btn-reset"></button>
          </form>
        </div>
      </>
    );
  }
}

컴포넌트는 UI를 나타내는 엘리먼트와 어플리케이션 로직을 포함한 상위 개념이다.
=> UI를 나타내는 엘리먼트 + 어플리케이션 로직 = 컴포넌트
=> "UI를 나타내는 엘리먼트"를 나타내는 부분이 밑에서 소개할 render()의 return 값이다.

  • 🚨🚨"리엑트 컴포넌트"로 만들어야 하는 이유

    • <input>에서 브라우져는 사용자가 "입력한 값"을 내부 상태로 관리하고 있다.
    • 그러나, 리엑트는 리엑트만의 상태관리가 필요하다.
      그렇게 하려면, 리엑트 컴포넌트라는 것을 사용해야 한다.
      => 즉, 입력값을 받아오려면, 리엑트 컴포넌트를 써야한다.
      => 입력한 값을 활용하려면(출력한다거나 API 호출 인자로 사용하려면) 어딘가에 저장해 두어야 한다.
      이를 위해, 리엑트에서는 컴포넌트라는 클래스의 도움이 필요하다.

주의 : 컴포넌트의 이름은 항상 대문자로 시작합니다.

React는 소문자로 시작하는 컴포넌트를 DOM 태그로 처리합니다.
예를 들어 <div />는 HTML div 태그를 나타내지만, <Welcome />은 컴포넌트를 나타내며 범위 안에 Welcome이 있어야 합니다.
자료출처 : Components와 Props 공식문서

리엑트를 로딩하게 되면, React라는 전역 공간이 생긴다.
또한, ReactDOM 이라는 전역 공간도 생긴다.
React.Component => 리엑트에서 제공해주는 클래스
C 가 대문자이다!! 주의하자!!

class App extends React.Component {
  render() {
    // render()는 리엑트 엘리먼트를 반환해야 한다.
    return (
      <>
        <header>
          <h2 className="container">검색</h2>
        </header>
        <div className="container">
          <form>
            <input type="text" placeholder="검색어를 입력하세요" autoFocus />
            <button type="reset" className="btn-reset"></button>
          </form>
        </div>
      </>
    );
  }
}

// element 대신에 JSX 문법으로 <App />으로 전달해줬다.
ReactDOM.render(<App />, document.querySelector("#app"));

리엑트에서는 리엑트가 제공하는 Component 클래스를 상속해서, render()를 만들어주면 컴포넌트가 된다.
그러나, App 이라는 class는 아직 컴포넌트는 아니다. 그저, 클래스일 뿐!!
컴포넌트가 되려면, 객체화 해줘야 한다.
그리고 나서, 리엑트 돔 렌더 함수에 전달해줘야 한다.


3. State - 입력 값을 기억하기

컴포넌트는 UI를 나타내는 엘리먼트와 어플리케이션 로직을 포함한 상위 개념이다.
=> UI를 나타내는 엘리먼트 + 어플리케이션 로직 = 컴포넌트

UI를 나타내는 엘리먼트 => render()의 return 값
어플리케이션 로직 => State 객체

즉, 컴포넌트는 State 객체를 변경하면서, 어플리케이션의 로직을 구현할 수 있다.
이것은 컴포넌트 내부에서만 접근할 수있는 것이다.
=> 생성자 함수에서 등록해야 한다.

class App extends React.Component {
  constructor() {
    super();

    this.state = {
     
      searchKeyword: "이게 들어가나?",

    };
  }

  render() {
    // render()는 리엑트 엘리먼트를 반환해야 한다.
    return (
      <>
        <header>
          <h2 className="container">검색</h2>
        </header>
        <div className="container">
          <form>
            <input
              type="text"
              placeholder="검색어를 입력하세요"
              autoFocus
              value={this.state.searchKeyword}
            />
            <button type="reset" className="btn-reset"></button>
          </form>
        </div>
      </>
    );
  }
}

// element 대신에 JSX 문법으로 <App />으로 전달해줬다.
ReactDOM.render(<App />, document.querySelector("#app"));

지금 State에서 하려는 것은 브라우져가 관리하던 입력 창의 값을 리엑트 컴포넌트가 관리하려고 한다.
그런 다음에 이게 리엑트 엘리먼트에 연결되어야 한다.
<input>value와 연결 됐으면, 좋겠다고 하자!!
value = "abc" 하면 값이 설정되는데, JSX 문법에서 JS 표현식을 넣으려면 {}를 넣어야 한다.
이제, 라이브 서버를 돌려보면, searchWordvalue 값이 들어가 있다.
그러나, 이 value가 지워지지도 않고, 변경되지도 않는다.
=> <input>이 전혀 반응하지 않는다.
이것은 <input>에서 change 이벤트가 발생했을 때, 여전히 그 기능을 브라우져에서 관리하고 있어서 생기는 문제이다.
즉, value 값만 리엑트에서 관리하고 있지 <input>change 이벤트는 브라우져가 관리하고 있기 때문에, 입력해도 반응하지 않는다.
value를 리엑트가 관리하는 것처럼 onChange 이벤트에 대해서도 리엑트가 관리를 해줘야 한다.


4. 상태를 갱신하기 - 이벤트 처리

<input> 에다가 무언가를 입력하면, change 이벤트가 발생한다.
HTML에서는 onchange라는 이벤트를 써서 change 이벤트를 받는다.
그러나, JSX는 onChange 로 받는다.

리엑트에서 이벤트를 처리하는 핸들러 이름들은 앞에 handle로 시작한다.

class App extends React.Component {
  constructor() {
    super();

    this.state = {
      searchKeyword: "이게 들어가나?",
    };
  }

  handleChangeInput(event) {
    this.state.searchKeyword = event.target.value;
    // 이 변화 값만으로는 여전히 input 값이 변화되지 않는다.
    // 리엑트는 필요한 순간에만 UI를 그리는 render()를 돌린다.
    // 즉, state를 변경했다고 해서, 다시 render()를 돌리지는 않는다.
    // 다시 말해서, 이 상황에서는 강제로 돌리기 위한 메소드를 써줘야 한다.
    this.forceUpdate();
  }

  render() {
    // render()는 리엑트 엘리먼트를 반환해야 한다.
    return (
      <>
        <header>
          <h2 className="container">검색</h2>
        </header>
        <div className="container">
          <form>
      // input 안에 onChange 이벤트를 달았다.
            <input
              type="text"
              placeholder="검색어를 입력하세요"
              autoFocus
              value={this.state.searchKeyword}
              onChange={(event) => this.handleChangeInput(event)}
            />
            <button type="reset" className="btn-reset"></button>
          </form>
        </div>
      </>
    );
  }
}

// element 대신에 JSX 문법으로 <App />으로 전달해줬다.
ReactDOM.render(<App />, document.querySelector("#app"));

그런데, 이러면 handleChangeInput(event)가 MVC 패턴 중 Model : value 변경, Controller : 화면 변경 하는 코드가 됐다.
뭔가 리엑트를 제대로 사용한 것처럼 보이지는 않는다.
리엑트를 좀 더 올바르게 사용하려면, 리엑트 컴포넌트 스스로가 이 state의 변화를 인지하고, 스스로 render()를 호출하도록 해야한다.

🚨🚨중요🚨🚨

  • 컴포넌트의 상태를 변경하려면, 직접 수정하지 말고, 컴포넌트 클래스가 제공하는 setState()하는 메소드를 사용하자!!
 handleChangeInput(event) {
    
    this.setState({
      searchKeyword: event.target.value,
    });
  }

이렇게 하면, 리엑트는 state가 변경됐는지를 인지하고 render()를 다시 그리게 된다. setState()는 " 컴포넌트의 상태를 변화시키겠다!! " 라고 하는 컴포넌트와의 직접적인 약속이다.

정리하면 ✍️,

  • 브라우져는 기본적으로 <input> 의 사용자 입력값을 스스로 관리한다.
  • 리액트에서 <input>을 제대로 다루려면 브라우져의 상태 관리를 리액트 안으로 가져와야 하는데 이 때 사용할 수 있는 것이 리액트 컴포넌트다.
  • React.Component 클래스는 상태 관리를 위한 내부 변수 state를 가지고 있다. 그리고 이 state를 변경하기 위해서는 setState()를 사용해줘야 render()가 자동적으로 UI를 다시 그린다.
  • 이렇게 <input> 자체의 상태 관리를 사용하지 않고, React.Component가 관리하는 것을 제어 컴포넌트(Controlled Component)라고 부른다.
  • 시행착오


검색어를 입력하면 x버튼이 보이고, 없으면 x버튼을 숨긴다

1. 조건부 렌더링

리액트에서 조건부 렌더링 하는 방식은 세 가지다.

JSX 문법에서 JS 코드를 쓰려면, {}를 쳐줘야 한다.

  • 엘리먼트 변수를 사용하는 방식
 this.state = {
      searchKeyword: "",
    };
  }

  handleChangeInput(event) {
    const searchKeyword = event.target.value;
    this.setState({ searchKeyword });
  }

render() {
    // TODO
    let resetButton = null;

    if (this.state.searchKeyword.length > 0) {
      resetButton = <button type="reset" className="btn-reset"></button>;
    }

    return (
      <>
        <header>
          <h2 className="container">검색</h2>
        </header>
        <div className="container">
          <form>
            <input
              type="text"
              placeholder="검색어를 입력하세요"
              autoFocus
              value={this.state.searchKeyword}
              onChange={(event) => this.handleChangeInput(event)}
            />
            {/* resetButton을 여기에 넣어줌 */}
            {resetButton}
            {/* <button type="reset" className="btn-reset"></button> */}
          </form>
        </div>
      </>
    );

searchKeyword<input>value와 바인딩되있다. 그리고 input 값이 변경될 때마다, change 이벤트가 일어나고, 그 이벤트를 관리하는 onChange 메소드가 발동하면서, 거기에 걸어놓은 handleChangeInput(event) 메소드가 발동하고, state 값이 변경되면서, render()도 다시 한 번 UI를 그려주게 된다.

render()에서는 searchKeyword의 length가 0 이상이면, 리셋 버튼 태그를 재할당되서 {resetButton}이 버튼으로 대체된다.

  • 삼항 연산자를 사용하는 방식
render() {
    // TODO

    return (
      <>
        <header>
          <h2 className="container">검색</h2>
        </header>
        <div className="container">
          <form>
            <input
              type="text"
              placeholder="검색어를 입력하세요"
              autoFocus
              value={this.state.searchKeyword}
              onChange={(event) => this.handleChangeInput(event)}
            />
////////////////////////
// JS 문법을 써주려면 {}을 써줘야한다.
// 주어진 조건이 참이면, 버튼을 보여주고!! 
// 조건이 거짓이면, null 출력!! 
            {this.state.searchKeyword.length > 0 ? (
              <button type="reset" className="btn-reset"></button>
            ) : null}
////////////////////////
          </form>
        </div>
      </>
    );
  }
}
  • && 연산자를 사용하는 방식
    뒤에 있는 것이 null 값 즉, 아무 것도 보이지 않는 것이라고 하면, 삼항 연산자보다 더 간단하게 작성할 수 있다.
// 삼항 연산자 
{this.state.searchKeyword.length > 0 ? (
              <button type="reset" className="btn-reset"></button>
            ) : null}

// && 연산자
{this.state.searchKeyword.length > 0 && (
              <button type="reset" className="btn-reset"></button>
            ) }

주어진 조건이 참이어야 && 다음에 딸려오는 구문이 실행되는 것이다.
조건이 거짓이라면, 아무 것도 일어나지 않는다.
그래서, 거짓 조건이 null일 때, 유용한 방식이다.


엔터를 입력하면, 검색 결과가 보인다

1. 폼 제출

폼 제출은 submit 이벤트로 받을 수 있다.
HTML에서는 onsubmit으로 submit 이벤트를 제어하듯이, React에서는 카멜케이스인 onSubmit으로 submit 이벤트를 제어할 수 있다.

// form 태그에 onSubmit을 넣어줬다.
  <form onSubmit={(event) => this.handleSubmit(event)}>
            <input
              type="text"
              placeholder="검색어를 입력하세요"
              autoFocus
              value={this.state.searchKeyword}
              onChange={(event) => this.handleChangeInput(event)}
            />
            {this.state.searchKeyword.length > 0 && (
              <button type="reset" className="btn-reset"></button>
            )}
          </form>
 handleSubmit(event) {
    // TODO
    // 브라우저에는 form태그의 submit 이벤트를 처리할 때,
    // 기본 동작이 서버로 페이지 요청을 다시하는 거다.
    // 그래서 다시 응답된 페이지를 브라우져가 다시 렌더링하고 있어서
    // 화면이 리프레시 되는 것이다.
    event.preventDefault();
    console.log(`${this.state.searchKeyword}에 관한  검색 폼이 제출됨`);
  }

=> 폼 제출은 일단 로그만 찍어두었다. 추가적인 기능들은 남은 요구사항들을 개발하면서, 추가할 예정이다.


X 버튼을 클릭하거나 검색어를 삭제하면 검색 결과를 삭제한다.

1. 폼 초기화

HTML에서는 reset 이벤트를 onreset으로 처리한다. 즉, 리엑트에서는 onReset으로 처리할 수 있다.

  • 시행착오
    • 사실, 이 부분에서 살짝 헷갈렸는데, reset<button>이 클릭됐을 때, 일어나기 때문에, <button>에 부착해야 되는 줄 알았는데, <form> 태그 내부에 있기 때문에 <form>에 부착해야 했다.
  • 1차 완성본
 handleReset(event) {
    // 검색창에 보이는 부분을 지워주기 위해
    const searchKeyword = "";
    // 저장된 검색 기록을 지워주기 위해
    this.state.searchKeyword = "";
    this.setState({ searchKeyword });
    // console.log("두번째 셋 스테이트 이후 ", this.state.searchKeyword);
  }

=> 물론, 이렇게 해도 작동은 한다. 그러나, 여기서 좀 더 코드 수를 줄여보자면, 이런 식으로도 코드를 짤 수 있다.

  • 2차 완성본
 handleReset() {
    // 저장된 검색 기록을 지워주기 위해
    this.state.searchKeyword = "";
    // 검색창에 보이는 부분을 지워주기 위해
    this.setState({ searchKeyword: "" });
    // console.log("두번째 셋 스테이트 이후 ", this.state.searchKeyword);
  }

=> 이렇게 하면, setState() 에서 searchKeyword를 초기화해주는 작업까지 할 수 있다.

그러나, 내가 한가지 큰 것은 간과하고 있었다.

  • 시행착오
    • this.state.searchKeywordthis.setState({ searchKeyword: "" }); 이후에도 초기화 되지 않아서, 메소드 내에서 빈문자열을 할당해줘야 했다.
  • setState()는 항상 비동기로 동작한다. 즉, 21 줄에서 setState()가 왔다고 해서, 22줄 console.log()에 해당 값이 바로 반영되지 않는다.
    이 리엑트 컴포넌트는 여러번 setState()를 호출하더라도 이것들을 모아뒀다가 나중에 늦게 실행을 한다.
    왜냐면, 여러번 호출을 하더라도 최소한의 변경만을 하기 위해서 state를 늦게 반영한다.
handleReset() {
    this.setState(
      () => {
        // 이 상태가 변경이 완료 된 후에, 
        return { searchKeyword: "" };
      },
      () => {
        // 그 값이 보장된 시점에서 실행 되는 부분
        console.log(
          "setState()가 완료 되는 시점에 돌아오는 콜백 함수 값 : ",
          this.state.searchKeyword
        );
      }
    );
  }

=> return { searchKeyword: "" };setState()에서 실행된 후에, 두번째 인자로 들어간 콜백 함수의 콘솔로그 값이 찍히는 구조이다.

Setstate()는 비동기로 동작한다는 점에 주의하자!!
자료출처 : setState() from 공식문서

이제 X버튼 클릭시, 검색 결과를 삭제하는 부분을 처리해줬으니, 검색어를 삭제해도 검색 결과가 지워지도록 처리해보자!!

"검색어를 삭제했다"라는 것은 인풋을 계속 입력하다가 아무것도 입력 안 하면, 삭제한 거임.
=> 입력한 검색어를 지울 때도 검색 결과를 숨겨야 한다.

handleChangeInput(event) {
    const searchKeyword = event.target.value;

    // 검색어가 삭제됐다고 판단되면, 리셋버튼과 똑같은 효과를 냄
    if (searchKeyword.length <= 0) {
      return this.handleReset();
    }

    this.setState({ searchKeyword });
  }

해당 github 링크

profile
좋은 길로만 가는 "조은길"입니다😁

0개의 댓글