3. 엘리먼트 렌더링
React에서 엘리먼트는 최소 단위 입니다.
이런 엘리먼트가 모여서 컴포넌트를 이루고 컴포넌트들이 모여서 프로덕트가 완성됩니다.
엘리먼트는 화면에 표시해줄 내용을 가지고 있습니다.
const element = <h1>Hello, world</h1>;
이러한 엘리먼트를 화면에 렌더링하려면 ReactDOM.render()를 이용하면 됩니다.
ReactDOM.render에는 엘리먼트와 루트 DOM 노드가 들어가게 됩니다.
<div id="root"></div>
const element = <h1>Hello, world</h1>
ReactDOM.render(element, document.getElementById('root'))
// ReactDOM.render(엘리먼트, 루트 DOM 요소)
React에서 엘리먼트는 불변객체 입니다. (엘리먼트가 생성된 이후에 자식이나 속성을 변경한다든가 하는 동작이 불가능!!!)
엘리먼트를 갱신하는 방법은 새로운 엘리먼트를 생성하고 이를 ReactDOM.render로 전달하는 방법 뿐입니다.
function tick() {
const element = <h1>Hello, world</h1>
ReactDOM.render(element, document.getElementById("root"))
}
setInterval(tick, 1000);
이렇게 매번 엘리먼트가 갱신이 되면 효율적으로 렌더링 하기 위하여
가상 DOM과 실제 DOM을 비교하여 변경된 부분만 실제 업데이트를 하게 됩니다.
가상 DOM의 재조정 알고리즘은 추후에 다뤄볼 수 있도록 하겠습니다.
아래 이미지는 엘리먼트가 매번 바뀌지만 전체가 변경되는 것이 아닌 내용이 변경된 텍스트 노드만 업데이트 되는 모습입니다.
4. Component와 Props
props는 속성을 나타내는 데이터입니다.
컴포넌트는 함수형 컴포넌트와 클래스형 컴포넌트로 나뉘어 집니다.
const test = () => {
return <h1>hihi</h1>;
}
위의 예시는 함수형 컴포넌트
class Test extends React.Component {
render() {
return <h1>hihi</h1>
}
}
위의 예시는 클래스형 컴포넌트 입니다.
클래스형 컴포넌트는 훅의 등장 이후에는 널리 사용되지 않는걸로 알고 있습니다.
훅은 아래와 같은 이유로 등장을 하였습니다.
컴포넌트에서 상태로직을 재사용하기 위해
클래스형 컴포넌트의 this는 혼잡을 가져왔고,
componentDidUpdate, componentDidMount와 같이 lifecycle에 중복되는 메서드들이 위치되어 지는 상황을 위해
아래의 예시부터는 함수형 컴포넌트를 기준으로 다루게 됩니다.
function Welcome(props) {
return <h1>Hello, {props.name}</h1>
}
const element = <Welcome name="Sara" />
ReactDOM.render(
element,
document.getElementById('root')
)
1. ReactDOM.render에서 Welcome 컴포넌트를 호출합니다.
2. Welcome 컴포넌트에 props로 name이 넘어갑니다.
3. Welcome 컴포넌트는 <h1>Hello, sara</h1>를 호출합니다.
4. ReactDOM은 <h1>Hello, Sara</h1>엘리먼트와 일치하도록 DOM을 효율적으로 업데이트 합니다(가상돔 개념)
!!! React 에서 커포넌트 이름은 항상 대문자여야 합니다. 소문자로 작성하게 되면 html 태그로 인식하게 됩니다.
컴포넌트 합성
여러 컴포넌트를 합성하여 하나의 컴포넌트를 만들 수 있습니다
function Welcome (props) {
return <h1>hihi</h1>
}
function App() {
return (
<Welcome />
<Welcome />
<Welcome />
)
}
컴포넌트 추출
function Comment(props) {
return (
<div className="Comment">
<div className="UserInfo">
<img className="Avatar"
src={props.author.avatarUrl}
alt={props.author.name}
/>
<div className="UserInfo-name">
{props.author.name}
</div>
</div>
<div className="Comment-text">
{props.text}
</div>
<div className="Comment-date">
{formatDate(props.date)}
</div>
</div>
);
}
위의 컴포넌트를 아래의 여러 컴포넌트로 추출할 수 있습니다.
function Comment(props) {
return (
<div className="Comment">
<UserInfo author={props.author} />
<div className="Comment-text">
{props.text}
</div>
<div className="Comment-date">
{formatDate(props.date)}
</div>
</div>
);
}
function UserInfo(props){
<div className="UserInfo">
<Avatar user={props.author} />
<div className="UserInfo-name">
{props.author.name}
</div>
</div>
}
function Avatar(props) {
<img className="Avatar"
src={props.user.avatarUrl}
alt={props.user.name}
/>
}
!!!! props의 이름은 사용될 context가 아닌 컴포넌트 자체의 관점에서 짓는 것을 권장합니다!!
=> 컴포넌트는 재사용될 수 있으므로 컴포넌트 자체의 관점에서 바라보는 것이 좋다고 생각합니다.
context의 관점에서 props의 이름을 설정하면 재사용 했을때 새로운 context의 이해관계와 충돌이 날 수 있다고 생각합니다.
Props는 읽기 전용입니다!
모든 react 컴포넌트는 자신의 Props를 다룰 때 반드시 순수 함수처럼 동작해야 합니다!!
순수함수란 동일한 입력값에 항상 동일한 결과를 반환하는 특성을 가지고 있습니다.
5. State와 생명주기
function Clock(props) {
return (
<div>
<h1>Hello, World</h1>
<h2>It is {props.date.toLocaleTimeString()}</h2>
</div>
)
}
function tick() {
ReactDOM.render(
<Clock date={new Date()} />,
document.getElemnetById("root")
)
}
setInterval(tick, 1000)
props를 외부에서 주입하지 않고 state에서 new Date() 값을 가지게 하였습니다.
저는 위의 함수를 아래와 같은 함수형 컴포넌트로 변경하였습니다.
function Clock() {
const [date, setDate] = useState(new Date())
useEffect(() => {
const tick = setInterval(() => setDate(new Date()), 1000)
return () => clearInterval(tick)
}, []);
return (
<div>
<h1>Hello, World</h1>
<h2>It is {date.toLocaleTimeString()}</h2>
</div>
);
}
ReactDOM.render(<Clock />, document.getElementById("root"));
위의 date는 커스텀 훅으로 뺄 수도 있을 것 같습니다! 재사용이 필요하다면 이동하여 위치 시킬 수 있습니다.
state는 props와 유사하지만, 비공개이며 컴포넌트에 의해 완전히 제어됩니다.
6. 이벤트 처리하기
react에서 이벤트는 소문자 대신 카멜케이스를 사용합니다.
<button onClick={activateLasers}>
Activate Lasers
</button>
새로 알게된 내용
=> react에서는 Return false를 반환해도 기본 동작을 반환할 수 없습니다.
html에서는 가능합니다.
<form onsubmit="console.log('You clicked submit.'); return false">
<button type="submit">Submit</button>
</form>
const Form = () => {
function handleSubmit(e) {
e.preventDefault();
console.log('You Clicked submit.');
}
return (
<form onSubmit={handleSubmit}>
<button type="submit">Submit</button>
</form>
)
}
7. 조건부 렌더링
React에서는 조건부 렌더링을 수행할 수 있습니다.
상황에 따라 로그인 컴포넌트나 로그아웃 컴포넌트를 리턴해 줄 수 있습니다.
아래와 같은 경우 Props로 전달받은 isLogin에 따라 LogoutComponent, LoginComponent를 리턴해 줄 수 있습니다.
const Component = ({ isLogin }) => {
if(isLogin) {
return <LogoutComponent />
}
return <LoginComponent />
}
저는 주로 Early return을 사용하거나 return문 내에서 논리연산자를 활용하여 컴포넌트를 뱉어냅니다.
const Component = ({ isLogin }) => {
return (
<div>
{isLogin && <LogoutComponent />}
{!isLogin && <LoginComponent />}
</div>
)
}
삼항연산자도 사용할 수 있습니다.
const Component = ({ isLogin }) => {
return (
<div>
{isLogin ? <LogoutComponent /> : <LoginComponent />}
</div>
)
}
8. 리스트와 key
리액트에서는 리스트 컴포넌트를 표현할 수 있습니다
const numbers = [1,2,3,4,5]
const Items = () => {
return (
{numbers.map(number => <div>{number}</div>)}
)
}
ReactDOM.render(<Items />, document.getElementById('root'))
위의 코드는 리액트에서 리스트를 표현하는 대표적인 방식입니다.
이때, key를 넣어주어야 합니다.
리액트에서는 같은 레벨의 자식들 간에는 Key값을 이용하여 재조정 해야할지 말지를 판단하기 때문입니다.
const numbers = [1,2,3,4,5]
const Items = () => {
return (
{numbers.map(number => <div key={number.toString()}>{number}</div>)}
)
}
ReactDOM.render(<Items />, document.getElementById('root'))
같은 형제 노드들에서 값이 변하지 않았다면 key 값으로 판단하여 리렌더링을 하지 않습니다.
가령 index를 key값으로 사용하는 경우가 있습니다.
이러한 경우에는 비효율적인 리렌더링이 발생할 수 있습니다.
아래의 경우는 맨 끝에 5가 포함된 모습입니다.
이러한 경우에는 1,2,3의 리렌더링이 발생하지 않습니다.
React에서는 Key를 통해 항목의 순서가 변경되지 않았다는 것을 알 수 있으니까요
<div key={1}>1</div>
<div key={2}>2</div>
<div key={3}>3</div>
<div key={1}>1</div>
<div key={2}>2</div>
<div key={3}>3</div>
<div key={4}>5</div>
아래의 경우는 4가 1앞에 추가된 모습입니다.
key값은 바뀌지 않았지만 값의 변화가 생긴것을 볼 수 있습니다.
모든 요소들의 Key값이 변경된 모습을 확인할 수 있습니다.
react에서는 같은 자식레벨에서 key를 통해 기존 트리와 이후 트리가 일치하는 지 확인합니다.
인덱스를 key로 사용하면 항목의 순서가 바뀌었을때 key값이 변경될 수 도 있습니다.
input을 체크했는데 key값이 변경되면 label만 변경된다 거나 하는 의도치 않은 상황을 맞이할 수 도 있습니다.
(React에서는 동일한 key값에 같은 DOM 요소를 보여줍니다)
<div key={1}>1</div>
<div key={2}>2</div>
<div key={3}>3</div>
<div key={1}>4</div>
<div key={2}>1</div>
<div key={3}>2</div>
<div key={4}>3</div>
9. 폼
html에서 input, select, textarea와 같은 폼 엘리먼트는 사용자의 입력을 기반으로 자신의 state를 가지게 됩니다.
React에서는 컴포넌트에서 state를 가지게 됩니다. state를 input, select, textarea에 value 속성으로 전달함으로써 제어컴포넌트로 관리할 수 있습니다.
값이 바뀔때마다 컴포넌트가 리렌더링 된다는 (상태가 변경되기 때문에) 단점이 있지만 편리함때문에 비제어 컴포넌트와 잘 고려하여 사용을 하는게 좋을듯 합니다.
cpu의 성능까지 고려하면 더 좋을듯 하구여
return (
<form onSubmit={this.handleSubmit}>
<label>
Name:
<input type="text" value={this.state.value} onChange={this.handleChange} />
</label>
<input type="submit" value="Submit" />
</form>
);
return (
<form onSubmit={this.handleSubmit}>
<label>
Essay:
<textarea value={this.state.value} onChange={this.handleChange} />
</label>
<input type="submit" value="Submit" />
</form>
);
return (
<form onSubmit={this.handleSubmit}>
<label>
Pick your favorite flavor:
<select value={this.state.value} onChange={this.handleChange}>
<option value="grapefruit">Grapefruit</option>
<option value="lime">Lime</option>
<option value="coconut">Coconut</option>
<option value="mango">Mango</option>
</select>
</label>
<input type="submit" value="Submit" />
</form>
);
10. State 끌어올리기
state 끌어올리기는 컴포넌트 내부에서 관리되던 상태를 공통으로 사용해야 하는 컴포넌트와의 가장 가까운 공통 부모로 이동시키는 것을 의미합니다.
리액트 공식문서에서는 온도를 화씨와 섭씨로 변경하여 보여주는 부분을 예시로 보여주었습니다.
class Calculator extends React.Component {
constructor(props) {
super(props);
this.handleCelsiusChange = this.handleCelsiusChange.bind(this);
this.handleFahrenheitChange = this.handleFahrenheitChange.bind(this);
this.state = {temperature: '', scale: 'c'};
}
handleCelsiusChange(temperature) {
this.setState({scale: 'c', temperature});
}
handleFahrenheitChange(temperature) {
this.setState({scale: 'f', temperature});
}
render() {
const scale = this.state.scale;
const temperature = this.state.temperature;
const celsius = scale === 'f' ? tryConvert(temperature, toCelsius) : temperature;
const fahrenheit = scale === 'c' ? tryConvert(temperature, toFahrenheit) : temperature;
return (
<div>
<TemperatureInput
scale="c"
temperature={celsius}
onTemperatureChange={this.handleCelsiusChange} />
<TemperatureInput
scale="f"
temperature={fahrenheit}
onTemperatureChange={this.handleFahrenheitChange} />
<BoilingVerdict
celsius={parseFloat(celsius)} />
</div>
);
}
}
이는 제가 의식하고 사용하려고 하는 방식중 하나입니다.
제가 상태를 관리하는 원칙은 다음과 같습니다.
이러한 근거는 상태는 이용되는 컴포넌트와 가까운 위치에 정의되어 있어야 한다는 기조에 근거합니다.
1. 상태는 사용하는 컴포넌트에서 관리
2. 같은 상태를 공유해야하는 컴포넌트와 가장 가까운 부모 컴포넌트에 상태를 이동 (상태 끌어올리기 사용)
3. 거리가 너무 멀다면 contextAPI 적용
4. Redux나 Recoil 적용 검토
상태와 컴포넌트간의 거리가 너무 멀다면 props drilling이 발생하고 코드의 흐름을 파악하기 어려워 집니다.
또한 불필요한 리렌더링이 발생할 수 있으므로 위와 같은 방식을 저는 선호합니다.
그중 첫단계인 상태 끌어올리기에 관한 내용이 10장 이었습니다.
'Frontend > React' 카테고리의 다른 글
[번역] React Atomic하게 바라보기 (0) | 2024.05.19 |
---|---|
React patterns 🤔 (0) | 2023.06.06 |
React what is JSX? (번역글) 🤔 (0) | 2022.04.27 |
React One Simple trick to optimize React re-renders (번역글) 🤔 (0) | 2022.04.27 |
React The State Reducer Pattern with React Hooks (번역글) 🤔 (0) | 2022.04.24 |