내가 만들고 있는 앱을 예로 들어 설명해보겠다.
웹과 상호작용을 하는게 많다면 이 상태라는것을 이용해야한다. 예를들어, 트위터나 인스타처럼 글을 작성하고 업로드 버튼을 누르면 내가 작성한 글이 웹에 다시 렌더링 되는것을 하려면 말이다. 리액트에서 이것을 하려면 두가지가 필요하다.
import classes from "./NewPost.module.css";
function NewPost() {
function changeBodyHandler(event) {
console.log(event.target.value);
}
return (
<form className={classes.form}>
<p>
<label htmlFor="body">Text</label>
<textarea id="body" required rows={3} onChange={changeBodyHandler} />
</p>
<p>
<label htmlFor="name">Your name</label>
<input type="text" id="name" required />
</p>
</form>
);
}
export default NewPost;
textarea
에서 onChange
로 이벤트 리스너를 등록하고 동적으로 함수를 넣어준다.
그리고 그 함수는 return
문 위에 작성한다. 그럼 리액트는 이걸 보고 이벤트 리스너를 textarea
에 붙여 주고, 이 함수 changeBodyHandler
함수를 이벤트 리스너의 타겟으로 설정해 change 이벤트
가 발생하면 실행시켜 준다. changeBodyHandler
안의 동작은 내가 원하는 동작으로.
changeBodyHandler
함수는 이벤트 객체를 자동으로 인자로 받고 인자로 들어온 이벤트는 정보를 가지고 있는데, 그건 발생한 이벤트 정보다. event.target 정보도 있는데, 여기에선 textarea
가 되겠다. 명시된 onChange 이벤트가 발생한 거라면.
여기에서 value 속성도 확인해 볼 수 있는데, event의 target인데 바로 textarea 요소고, 그 정보에 value라는 속성이 있는데 값은 바로 사용자가 입력한 내용이다.
여기까지가 이벤트 리스너를 다룬 내용이고 다음부터 state에 대해 설명이다.
사실 이벤트 리스너 없이도 상태를 다룰 수 있지만 보통 상태를 사용할떄는 특정 이벤트가 일어났을때다.
✍ 요약
여기까지는 상태가 아닌 이벤트를 등록하는 과정이었다. 8.2 코드처럼 함수를 만들고
on
으로 시작하는 속성을 추가하고ex) onChange
실행해야하는 함수를 하나의 값으로 속성에 지정해 준다.
그러면 리액트가 알아서 발생한 이벤트의 객체를 이 함수로 넘겨 준다.
✍ 문제
Event Handler도 만들었고, 동적으로 함수도 실행시켜서 콘솔창에 변수가 매번 업데이트 되는것도 확인했다. 그런데 실제로 키보드를 눌러 이벤트가 일어나도 아무런 변화가 없다. 왜냐면 리액트는 해당 jsx코드를 단 한 번만 실행하기 때문이다.(이 컴포넌트 함수가 실행되는 첫번째에만!) 그러니까 리액트는 내가 키보드를 눌렀단 사실을 모른다. 어떻게 해야할까?
✍ 해결방법
답 : useState
란 함수를 사용해야한다.
리액트를 다시 실행해서 업데이트를 해야한다. 그러기 위해선 리액트에서 제공하는 기능을 이용해야하는데, 변수를 특별한 방법으로 생성해야 한다. 여기선 useState
라는 함수를 사용할 것이다. 그리고 이것은 리액트 훅이라고 불린다.
✍ 리액트 훅
리액트엔 빌트인 함수가 많은데 함수들 이름이 다 use로 시작한다. 이 함수들은 표준 자바스크립트 함수들인데 리액트 훅이라고 불린다.
컴포넌트 함수 안에서 실행해야만 한다. 일반 자바스크립트 함수 안에선 실행시킬 수 없다. 그리고 이 훅 함수들은 해당 컴포넌트의 뭔가를 바꾸거나 컴포넌트 관련 리액트가 다르게 작동해야 할 때 쓴다.
✍ useState 특징
- 배열 (2개 항목)을 반환한다.
array[0] : 현재 상태의 값
array[1] : 상태를 업데이트하는 함수. (이 함수를 호출해서 상태의 값을 변경할 수 있다. )
- 상태 업데이트 함수를 호출하면 메모리에 새 상태 값을 저장할 뿐 아니라 리액트가 그 함수를 다시 호출한다.
즉 , 컴포넌트 함수가 다시 호출되니까 상태의 현재 값을 참조하게 된다. 따라서 JSX 코드 가장 최신 버전의 스냅샷을 갖게 되는것이고, 그 말은 결국 리액트가 업데이트를 해서 UI가 최신 상태로 되는것이다.
- 새롭게 업데이트 하기 위해서 값을 새롭게 할당할 필요가 없다. 직접 할당하는 대신에 이 업데이트 함수, setEnteredBody를 호출해서 새 상태 값을 인자로 setEnteredBody에 넘겨주면 된다. 이렇게 하면 새 상태 값은 저장되고 이 컴포넌트 함수는 다시 실행되고 enteredBody는 새 값을 가지게 된다. 그래서 const로 변수를 받고, 항상 새로운 상수(const)가 된다.
✍ 위 설명 종합한 NewPost.jsx 코드
import { useState } from "react";
import classes from "./NewPost.module.css";
function NewPost() {
const [enteredBody, setEnteredBody] = useState("");
function changeBodyHandler(event) {
setEnteredBody(event.target.value);
}
return (
<form className={classes.form}>
<p>
<label htmlFor="body">Text</label>
<textarea id="body" required rows={3} onChange={changeBodyHandler} />
</p>
<p>
<label htmlFor="name">Your name</label>
<input type="text" id="name" required />
</p>
</form>
);
}
export default NewPost;
성공!
✍ 문제 발생
위의 NewPost.jsx 코드에서 useState를 이용해 키보드 이벤트가 수신되어 내가 쓴 글자가 렌더링 되는것 까진 성공했다. 그런데 문제가 생겼다. 내가 원하는 것은 새로운 포스트 작성란 밑에 글자가 써지는 것이 아니라, 게시글 목록들에 글자가 등록 되는걸 원했다. 그런데 작성된 스크립트가 달랐다. 다시 말해서 NewPost
컴포넌트에 state
가 필요한것이 아니라 PostList
컴포넌트에서 필요한 것이다. PostList 컴포넌트에 UseState를 사용하면 될 것 같지만 아니다. 이벤트 핸들러가 여전히 NewPost 컴포넌트에 있다. 어떻게 해결해야할까?
✍ PostList.jsx
✍ 해결방법
답 : 상태 올리기를 해야한다.
A라는 컴포넌트에서 관리하는 상태가 B라는 컴포넌트에 필요하다면 상태를 올려줘야 하는데 어디로 올리느냐? 상태를 이용할 두 컴포넌트 다 접근한 컴포넌트로 한다.
위 사진처럼 우리 코드에서는 PostsList 컴포넌트가 상태 정보를 Post 컴포넌트의 body 속성으로 전달해 줘야 한다. PostsList 컴포넌트는 또 NewPost 컴포넌트에 접근할 수 있고, NewPost 컴포넌트가 상태가 업데이트 되는 곳이기도 하고.
그럼 이제 할것은
NewPost
에서 상태 관련 코드를 지우기 (이벤트 처리함수 포함)- useState Import 문 지우기
- props 이용해서 이벤트 리스너 속성에 추가하기.
- 해당 컴포넌트의
props
의 객체에 데이터를 담아줄 곳에서 속성을 추가한다. 여기서는PostList
에서NewPost
컴포넌트를 사용하므로 거기서 작성해준다.
✍ 좌측이 수정전 우측이 수정 후 NewPost.jsx
✍ 좌측이 수정전 우측이 수정 후 PostList.jsx
쉽게 말해서 bodyChangeHandler
함수를 onBodyChange
담아서 객체로써 NewPost
컴포넌트에 전달하는것임.
이렇게 하면 상태가 변경되는 컴포넌트에서 상태를 업데이트할 수 있고 동시에 이벤트가 발생하는 곳에 리스너를 둘 수 있는 것이다. props으로.
NewPost UI를 모달창처럼 보이게 만들어보려고 한다. CSS를 이용해서 만들어도 되지만, 특별한 "children" 프로퍼티를 이용해서 해보겠다. 아래코드처럼 NewPost
를 Modal
로 감쌌다.(래퍼 컴포넌트) 아직 Modal
컴포넌트를 만들지 않았지만, 복잡한 프로젝트에서는 해당 코드처럼 오버레이 형식으로 만들기 때문이다. ex) 경고창, 모달창 등등
✍ PostList.jsx
return (
<>
<Modal>
<NewPost
onBodyChange={bodyChangeHandler}
onAuthorChange={authorChangeHandler}
/>
</Modal>
<ul className={classes.posts}>
<Post author={enteredAuthor} body={enteredBody} />
<Post author="Manel" body="the second props pratice!" />
</ul>
</>
);
✍ Modal.jsx
import classes from "./Modal.module.css";
function Modal() {
<>
<div className={classes.backdrop}>
<dialog className={classes.modal}></dialog>
</div>
</>;
}
export default Modal;
✍ 문제 발생
그래서 이제 Modal 컴포넌트를 만들고 위에서 처럼 PostList
컴포넌트에 적용했는데 원하는 결과가 나오지 않았다.
왜냐하면 콘텐츠를 래핑했을 때 그게 컴포넌트든 아니면 다른 기본 내장 요소든 리액트는 컴포넌트의 어느 위치에 래핑된 콘텐츠를 표시해야 할지 알 수 없기 때문이다. 그래서 래핑된 콘텐츠를 어디에 렌더링 할지 리액트에게 알려줘야한다.
✍ 해결방법
특수한 프로퍼티인 props.children
속성을 이용한다.
아래 코드를 통해 설명하자면, 좌측의 props.children
속성은 우측의 Modal
컴포넌트가 감싸고 있는 모든것들을 뜻한다. (단, 해당 컴포넌트로 감싸고 있어야한다.)
props.children
을 이용해서 dialog
부분에 Modal
로 감싼 부분인 NewPost
를 렌더링 하라고 알려준 것이다.<dialog open={true} className={classes.modal}>
<NewPost
onBodyChange={bodyChangeHandler}
onAuthorChange={authorChangeHandler}
/>
이렇게 하면 폼이 오버레이 스타일로 나오게 된다. 다른 콘텐츠 위에 표시된다.