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>
에 텍스트를 입력하면, 입력한 값이 출력되서 나올 것이다.
위에 코드를 리엑트 컴포넌트로 만들어보자!!
// 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
는 아직 컴포넌트는 아니다. 그저, 클래스일 뿐!!
컴포넌트가 되려면, 객체화 해줘야 한다.
그리고 나서, 리엑트 돔 렌더 함수에 전달해줘야 한다.
컴포넌트는 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 표현식을 넣으려면 {}
를 넣어야 한다.
이제, 라이브 서버를 돌려보면, searchWord
의 value
값이 들어가 있다.
그러나, 이 value
가 지워지지도 않고, 변경되지도 않는다.
=> <input>
이 전혀 반응하지 않는다.
이것은 <input>
에서 change
이벤트가 발생했을 때, 여전히 그 기능을 브라우져에서 관리하고 있어서 생기는 문제이다.
즉, value
값만 리엑트에서 관리하고 있지 <input>
의 change
이벤트는 브라우져가 관리하고 있기 때문에, 입력해도 반응하지 않는다.
value
를 리엑트가 관리하는 것처럼 onChange
이벤트에 대해서도 리엑트가 관리를 해줘야 한다.
<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
버튼을 숨긴다리액트에서 조건부 렌더링 하는 방식은 세 가지다.
- 엘리먼트 변수를 사용하는 방식
- 삼항 연산자를 사용하는 방식
&&
연산자를 사용하는 방식
자료 출처 : 조건부 렌더링 from 리엑트 공식문서
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
일 때, 유용한 방식이다.
폼 제출은 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}에 관한 검색 폼이 제출됨`);
}
=> 폼 제출은 일단 로그만 찍어두었다. 추가적인 기능들은 남은 요구사항들을 개발하면서, 추가할 예정이다.
HTML에서는 reset
이벤트를 onreset
으로 처리한다. 즉, 리엑트에서는 onReset
으로 처리할 수 있다.
- 시행착오
- 사실, 이 부분에서 살짝 헷갈렸는데,
reset
가<button>
이 클릭됐을 때, 일어나기 때문에,<button>
에 부착해야 되는 줄 알았는데,<form>
태그 내부에 있기 때문에<form>
에 부착해야 했다.
handleReset(event) {
// 검색창에 보이는 부분을 지워주기 위해
const searchKeyword = "";
// 저장된 검색 기록을 지워주기 위해
this.state.searchKeyword = "";
this.setState({ searchKeyword });
// console.log("두번째 셋 스테이트 이후 ", this.state.searchKeyword);
}
=> 물론, 이렇게 해도 작동은 한다. 그러나, 여기서 좀 더 코드 수를 줄여보자면, 이런 식으로도 코드를 짤 수 있다.
handleReset() {
// 저장된 검색 기록을 지워주기 위해
this.state.searchKeyword = "";
// 검색창에 보이는 부분을 지워주기 위해
this.setState({ searchKeyword: "" });
// console.log("두번째 셋 스테이트 이후 ", this.state.searchKeyword);
}
=> 이렇게 하면, setState()
에서 searchKeyword
를 초기화해주는 작업까지 할 수 있다.
그러나, 내가 한가지 큰 것은 간과하고 있었다.
- 시행착오
this.state.searchKeyword
가this.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 });
}