[ Component & Rendering ]
▶ Component
컴포넌트는 리액트의 핵심 빌딩 블록 중 하나 즉 리액트에서 개발할 모든 애플리케이션은 컴포넌트라는 조각으로 구성됨
컴포넌트는 UI 구축 작업을 훨씬 쉽게 만들어줌
컴포넌트란 리액트의 핵심 빌딩 블록 중 하나로 UI 요소를 표현하는 최소한의 단위이며 화면의 특정 부분이 어떻게 생길지 정하는 선언체
컴포넌트를 생성하고 보여지고자 하는 UI 요소를 컴포넌트 내부에서 JSX를 통해 선언하면 이를 리액트가 화면에 그려줌
리액트 컴포넌트가 선언체라는 개념은 아주 중요함
이유는 리액트의 컴포넌트기반 개발 이전에는 브라우저에서 동적으로 변하는 UI를 표현하기 위해 직접 DOM 객체를 조작하는 명령형 프로그래밍 방식으로 구현했기 때문
「명령형은 어떻게(How)를 중요시 여겨서 프로그램의 제어의 흐름과 같은 방법을 제시하고 목표를 명시하지 않는 형태
선언형은 무엇(What)을 중요시 여겨서 제어의 흐름보다는 원하는 목적을 중요시 여기는 형태」
DOM (명령형 프로그래밍)
명령형으로 작성된 코드의 경우 Hello, World!를 출력하기 위해 컴퓨터가 수행하는 절차를 일일히 코드로 작성해주어야 함
// Hello, World! 화면에 출력하기
// 순수 javaScript 명령형 코드
const root = document.getElementById('root');
const header = document.createElement('h1');
const headerContent = document.createTextNode(
'Hello, World!'
);
header.appendChild(headerContent);
root.appendChild(header);
리액트 (선언형 프로그래밍)
React 코드의 경우 내가 UI을 선언하고 render 함수를 호출하면 React가 알아서 절차를 수행해 화면에 출력
즉, 화면에 어떻게 그려야할지는 React 내부에 잘 숨겨져 추상화되어 있음
// React 코드 (선언적인)
const header = <h1>Hello World</h1>; // jsx
ReactDOM.render(header, document.getElementById('root'));
DOM을 직접 조작하여 명령형 프로그래밍 방식으로 작성하던 코드가 나쁘다는게 아님
카운터 예시와 같이 격리된 예제에서는 차라리 리액트와 같은 UI 라이브러리를 사용하지 않고 만드는게 더 빠르고 전체적인 번들 사이즈 측면에서도 더 효율적인 방법일수 있음. 하지만 더 복잡한 UI 시스템에서는 관리하기가 기하급수적으로 어려워짐
▶ 렌더링
리액트에서 렌더링이란 컴포넌트가 현재 props와 state의 상태에 기초하여 UI를 어떻게 구성할지 컴포넌트에게 요청하는 작업을 의미
컴포넌트를 주방에서 요리를 준비하는 주방장, 리액트는 손님으로부터 주문받아 주방에 전달하고 완성된 요리를 손님에게 서빙하는 웨이터로 그리고 손님이 주문하고 주방장이 만드는것이 요리가 아닌 UI라고 생각하면
- UI - 음식
- 컴포넌트 - 음식을 만드는 주방장
- 리액트 - 웨이터
렌더링을 일어나는 프로세스를 이렇게 설명할 수 있음
- 렌더링 일으키는 것은(triggering) - UI를 주문하고 주방으로 전달하는 것
- 렌더링한다는 것은(rendering) - 주방에서 컴포넌트가 UI를 만들고 준비하는 것
- 렌더링 결과는 실제 DOM에 커밋한다는 것은(commit) - 리액트가 준비된 UI를 손님 테이블에 올려놓는 것
렌더링 트리거
렌더링이 발생하는 경우
- 첫 리액트 앱을 실행했을 때
- 현재 리액트 내부에 어떤 상태(state)에 변경이 발생했을 때.
- 컴포넌트 내부 state가 변경되었을 때
- 컴포넌트에 새로운 props가 들어올 때,
- 상위 부모 컴포넌트에서 위에 두 이유로 렌더링이 발생했을 때
리액트 앱이 실행되고 첫 렌더링이 일어나면 리액트는 컴포넌트의 루트에서 시작하여 아래쪽으로 쭉 훑으며 컴포넌트가 반환하는 JSX 결과물을 DOM 요소에 반영함
리렌더링
첫 렌더링은 자동으로 일어난 것. 리액트 앱이 실행되면 리액트는 전체 컴포넌트를 렌더링하고 결과물을 DOM에 반영해 브라우저상에 보여줌. 첫 렌더링을 끝난 이후에 추가로 렌더링을 트리거 하려면 상태를 변경해주면 됨
컴포넌트 상태에 변화가 생기면 리렌더링이 발생함. 이때 여러 상태가 변경됐다면 리액트는 이를 큐 자료구조에 넣어 순서를 관리함
주방 예시를 다시 들어보면
- 리렌더링은 음식점 손님이 첫 주문 이후에 갈증이 생겨 추가로 음료를 주문하거나 처음 받은 음식이 마음에 들지 않아 새로운 메뉴를 주문하는 것과 같음
- 새로운 UI주문(리렌더링)이 일어나면 리액트가 변경된 내용을 주방에 있는 요리사인 컴포넌트에 전달하고 컴포넌트는 새로운 변경된 주문을 토대로 새로운 요리(UI)를 만듬
- 새롭게 만들어진 요리(렌러딩 결과)는 리액트에 의해 다시 손님 테이블에 올려짐(DOM에 반영 - commit phase)
▶ 브라우저 렌더링
브라우저의 렌더링과 리액트의 렌더링은 엄연히 다른 독립적인 프로세스
렌더링이 완료되고 React가 DOM을 업데이트한 후 브라우저는 화면을 그림
이 프로세스를 "브라우저 렌더링"이라고 하지만 혼동을 피하기 위해 "페인팅"이라고도 함
[ Styling ]
▶ 컴포넌트 스타일링
컴포넌트 파일에서 CSS 분리하기
JSX는 사실 HTML과 굉장히 닮아 있기 때문에 방법이 크게 다르지 않음
한가지 차이점이 있다면 class → className 을 사용한다는 점
import React from "react";
function App() {
const style = {
padding: "100px",
display: "flex",
gap: "12px",
};
const users = [
{ id: 1, age: 30, name: "송중기" },
{ id: 2, age: 24, name: "송강" },
{ id: 3, age: 21, name: "김유정" },
{ id: 4, age: 29, name: "구교환" },
];
return (
<div style={style}>
{users.map((user) => {
return <Square user={user} key={user.id} />;
})}
</div>
);
}
export default App;
컴포넌트 파일에서 className을 넣어줌
이 컴포넌트에서 적용할 CSS파일을 import 해줌(경로가 무척 중요하니 잘 확인해야함)
import React from "react";
import Square from "./components/Square.js";
import "./App.css"; // 🔥 반드시 App.css 파일을 import 해주어야 함
function App() {
const users = [
{ id: 1, age: 30, name: "송중기" },
{ id: 2, age: 24, name: "송강" },
{ id: 3, age: 21, name: "김유정" },
{ id: 4, age: 29, name: "구교환" },
];
return (
<div className="app-style">
{users.map((user) => {
return <Square user={user} key={user.id} />;
})}
</div>
);
}
export default App;
기존 style 변수에 넣어줬던 스타일 프로퍼티들을 별도에 CSS 파일로 옮길때 기존에는 style을 JS객체로 작성했었기 때문에 CSS 프로퍼티의 값을 따옴표(””)로 감싸주었지만 CSS 문법에서는 따옴표를 다 지워야 함
<!--src/App.css-->
.app-style {
padding: 100px;
display: flex;
gap: 12px;
}
하면 분리 완성
[ 반복되는 컴포넌트 처리하기 ]
▶ 리액트에서의 map
map은 자바스크립트 메서드
import React from "react";
const vegetables = ["감자", "고구마", "오이", "가지", "옥수수"];
return (
<div className="app-style">
{vegetables.map((vegetableName) => {
return (
<div className="square-style" key={vegetableName}>
{vegetableName}
</div>
);
})}
</div>
);
};
export default App;
JSX 부분에서 JS코드를 작성할 것이기 때문에 { } 로 먼저 감싸고 시작
JSX에서 map() 은 배열의 모든 요소를 순회함. 그래서 클라이언트에서는 배열 형태의 데이터를 활용해서 화면을 그려주는 경우가 많고, 이때 배열의 값들로 동적으로 컴포넌트를 만들수 있음
map을 사용하니 중복된 코드가 사라지고 1개의 컴포넌트를 이용하면서 그 안에서 <div>{vegetableName}</div> 가 순차적으로 보여지고 있음
▶ 조금 더 복잡한 데이터 다뤄보기
객체가 담긴 배열 다뤄보기
import React from 'react';
import './App.css';
// User 컴포넌트를 분리해서 구현
function User(props) {
return (
<div>{props.user.age}살 - {props.user.name}</div>
);
}
const App = () => {
const users = [
{ id: 1, age: 30, name: '송중기' },
{ id: 2, age: 24, name: '송강' },
{ id: 3, age: 21, name: '김유정' },
{ id: 4, age: 29, name: '구교환' },
];
return (
<div className="app-container">
{users.map((user) => {
return <User user={user} key={user.id} />;
})}
</div>
);
};
export default App;
App 컴포넌트에서는 user.map( )을 통해 user의 정보를 순회하고 각각의 user 정보를 User 컴포넌트로 주입해줌
추가/삭제를 위한 준비
import React from 'react';
import './App.css'; //
function User(props) {
return (
<div>{props.user.age}살 - {props.user.name}</div>
);
}
const App = () => {
const [users, setUsers] = useState([
{ id: 1, age: 30, name: '송중기' },
{ id: 2, age: 24, name: '송강' },
{ id: 3, age: 21, name: '김유정' },
{ id: 4, age: 29, name: '구교환' },
]);
const [name, setName] = useState(''); // <-- 유저의 입력값을 담을 상태
const [age, setAge] = useState('');
return (
<div className="app-container">
<input value={name}
placeholder="이름을 입력해주세요"
// 인풋 이벤트로 들어온 입력 값을 name의 값으로 업데이트
onChange={(e) => setName(e.target.value)}
/>
<input value={age}
placeholder="나이를 입력해주세요"
// 인풋 이벤트로 들어온 입력 값을 age의 값으로 업데이트
onChange={(e) => setName(e.target.value)}
/>
{users.map((user) => {
return <User user={user} key={user.id} />;
})}
</div>
);
};
export default App;
User 추가
import React from 'react';
import './App.css';
function User(props) {
return (
<div className="user-card">
<div>{props.user.age}살 - </div>
<div>{props.user.name}</div>
</div>
);
}
const App = () => {
const [users, setUsers] = useState([
{ id: 1, age: 30, name: '송중기' },
{ id: 2, age: 24, name: '송강' },
{ id: 3, age: 21, name: '김유정' },
{ id: 4, age: 29, name: '구교환' },
]);
const [name, setName] = useState('');
const addUserHandler = () => {
const newUser = {
id: users.length + 1,
age: age,
name: name,
};
setUsers([...users, newUser]);
};
return (
<div className="app-container">
<input
value={name}
// 인풋 이벤트로 들어온 입력 값을 name의 값으로 업데이트
onChange={(e) => setName(e.target.value)}
/>
{users.map((user) => {
return <User user={user} key={user.id} />;
})}
<button onClick={addUserHandler}>추가하기</button>
</div>
);
};
export default App;
user 삭제
import React from 'react';
import './App.css';
function User(props) {
return (
<div className="user-card">
<div>{props.user.age}살 - </div>
<div>{props.user.name}</div>
<button onClick={() => props.handleDelete(props.user.id)}>
삭제하기
</button>
</div>
);
}
const App = () => {
const [users, setUsers] = useState([
{ id: 1, age: 30, name: '송중기' },
{ id: 2, age: 24, name: '송강' },
{ id: 3, age: 21, name: '김유정' },
{ id: 4, age: 29, name: '구교환' },
]);
const [name, setName] = useState(''); // <-- 유저의 입력값을 담을 상태
const addUserHandler = () => {
const newUser = {
id: users.length + 1,
age: 30,
name: name,
};
setUsers([...users, newUser]);
};
const deleteUserHandler = (id) => {
const newUserList = users.filter((user) => user.id !== id);
setUsers(newUserList);
};
return (
<div className="app-container">
<input
placeholder="이름을 입력해주세요"
value={name}
// 인풋 이벤트로 들어온 입력 값을 name의 값으로 업데이트
onChange={(e) => setName(e.target.value)}
/>
{users.map((user) => {
return <User user={user} key={user.id} handleDelete={deleteUserHandler}/>;
})}
<button onClick={addUserHandler}>추가하기</button>
</div>
);
};
export default App;
컴포넌트 분리
import React from 'react';
import './App.css';
//1. 버튼 컴포넌트 생성
function CustomButton(props) {
return <button onClick={props.onClick}>{props.children}</button>;
}
// User 컴포넌트를 분리해서 구현
function User(props) {
return (
<div className="user-card">
<div>{props.user.age}살 - </div>
<div>{props.user.name}</div>
//2. 버튼을 컴포넌트로 바꾸기
<CustomButton onClick={() => props.handleDelete(props.user.id)}>
삭제하기
</CustomButton>
</div>
);
}
const App = () => {
const [users, setUsers] = useState([
{ id: 1, age: 30, name: '송중기' },
{ id: 2, age: 24, name: '송강' },
{ id: 3, age: 21, name: '김유정' },
{ id: 4, age: 29, name: '구교환' },
]);
const [name, setName] = useState(''); // <-- 유저의 입력값을 담을 상태
const addUserHandler = () => {
const newUser = {
id: users.length + 1,
age: 30,
name: name,
};
setUsers([...users, newUser]);
};
const deleteUserHandler = (id) => {
const newUserList = users.filter((user) => user.id !== id);
setUsers(newUserList);
};
return (
<div className="app-container">
<input
placeholder="이름을 입력해주세요"
value={name}
// 인풋 이벤트로 들어온 입력 값을 name의 값으로 업데이트
onChange={(e) => setName(e.target.value)}
/>
{users.map((user) => {
return <User user={user} key={user.id} handleDelete={deleteUserHandler}/>;
})}
//3. 버튼을 컴포넌트로 바꾸기
<CustomButton onClick={addUserHandler}>추가하기</CustomButton>
</div>
);
};
export default App;
버튼 색상 변경하기
// Button 컴포넌트 부분만
function CustomButton(props) {
const {color, onClick, children} = props
if (color)
return (
<button
style={{ background: color, color: "white" }}
onClick={onClick}
>
{children}
</button>
);
return <button onClick={onClick}>{props.children}</button>;
}
분리한 컴포넌트를 이용해 컴포넌트를 호출할때 색상에 대한 정보를 props로 전달받아 컴포넌트에 알맞은 색깔을 추가할 수 있도록 구현
▶ key
브라우저 콘솔에 에러가 뜬다면
리액트에서 map을 사용하여 컴포넌트를 반복 렌더링 할 때는 반드시 컴포넌트에 key를 넣어주어야 함(아니면 에러)
key가 필요한 이유는 React에서 컴포넌트 배열을 렌더링했을 때 각각의 원소에서 변동이 있는지 알아내려고 사용하기 때문. 만약 key가 없다면 React는 가상돔을 비교하는 과정에서 배열을 순차적으로 비교하면서 변화를 감지하려 함. 하지만 key가 있으면 이 값을 이용해서 어떤 변화가 일어났는지 더 빠르게 알아낼 수 있게 됨. 즉, key값을 넣어줘야 React의 성능이 더 최적화 됨
key를 넣는 방법
key는 아래와 같이 넣어주면 됩니다. props 처럼 생겼지만, props로 설정하지 않아도 key를 넣을 수 있음
<div style={style}>
{users.map((user) => {
return <User user={user} key={user.id} />;
})}
</div>
map에서 지원해주는 index를 사용해서 key를 넣는 경우가 있는데 그것은 좋지 않은 방식
항목에 순서가 바뀔수 있는 경우 key에 인덱스를 사용하는 것은 권장하지 않음
이로 인해 성능이 저하되거나 컴포넌트의 state와 관련된 문제가 발생할 수 있기 때문
리스트 항목에 명시적으로 key를 지정하지 않으면 React는 기본적으로 인덱스를 key로 사용함
▶ Null vs Undefined
undefined와 null: 둘 다 없음을 의미
undefined
1. 사용자 지정
2. 자바스크립트 엔진에서 자동부여
- 변수에 값이 지정되지 않은 경우, 데이터 영역의 메모리 주소를 지정하지 않은 식별자에 접근할 때
- .이나 []로 접근하려 할 때, 해당 데이터가 존재하지 않는 경우
- return 문이 없거나 호출되지 않는 함수의 실행 결과
3. 비어있는 요소와 undefined
// 비어있는 요소와, undefined를 할당한 요소는 다른것!
var arr1 = [];
arr1.length = 3;
console.log(arr1);
var arr2 = new Array(3);
console.log(arr2);
var arr3 = [undefined, undefined, undefined];
console.log(arr3);
4. 2가지 역할을 가진 undefined 혼돈의 위험성
- undefined로 나오는 이 변수가 필요에 의해 할당한건지 JS엔진이 반환한건지 구분할 수 없음
- ‘없다’를 명시적으로 표현할 때는 undefined를 사용하면 안됨
null
- 용도 : ‘없다’를 명시적으로 표현할 때
- 주의 : typeof null
var n = null;
console.log(typeof n);
//동등연산자(equality operator)
console.log(n == undefined); //true
console.log(n == null); //true
//일치연산자(identity operator)
console.log(n === undefined); //false
console.log(n === null); //true
[ 컴포넌트 분리해서 구현하기 ]
컴포넌트가 모두 App.js라는 파일 한곳에 작성되어 있기 때문에 발생하는 몇가지 문제점들
- App.js 파일의 역할이 명확하지 않음
- 컴포넌트 분리를 통해 가독성을 높였지만 두 컴포넌트의 사이즈가 커지거나 혹은 또 다른 컴포넌트를 작성하게 된다면 가독성은 금방 떨어지게 될 것으로 보임
- 현재 프로젝트 구조에서 User 컴포넌트, Button 컴포넌트가 어디에 작성되어 있는지 찾기가 힘듬
- 특히 작성자가 아닌 다른 개발자가 App.js 파일을 보고 User 컴포넌트, Button 컴포넌트가 해당 파일에 작성되어 있다고 유추하기 쉽지 않아 보임
일반적으로, 계속 여러번 렌더링하여 기능을 재사용하는 컴포넌트들은 따로 분리해서 사용
// 경로: src/components/Button.js
function Button(props) {
switch (props.color) {
case 'green': {
return (
<button
style={{ background: 'green', color: 'white' }}
onClick={props.onClick}
>
{props.children}
</button>
);
}
case 'red': {
return (
<button
style={{ background: 'red', color: 'white' }}
onClick={props.onClick}
>
{props.children}
</button>
);
}
default: {
return <button onClick={props.onClick}>{props.children}</button>;
}
}
}
export default Button;
// 외부 모듈(파일)에서 Sqaure 컴포넌트를 사용할 수 있게 export(내보내기)해줘야 함
// 경로: src/App.js
import React from 'react';
import Button from './components/Button.js';
function User(props) {
return (
<div className="user-card">
<div>{props.user.age}살 - </div>
<div>{props.user.name}</div>
<Button onClick={() => props.handleDelete(props.user.id)}>
삭제하기
</Button>
</div>
);
}
const App = () => {
const [users, setUsers] = useState([
{ id: 1, age: 30, name: '송중기' },
{ id: 2, age: 24, name: '송강' },
{ id: 3, age: 21, name: '김유정' },
{ id: 4, age: 29, name: '구교환' },
]);
const [name, setName] = useState('');
const addUserHandler = () => {
const newUser = {
id: users.length + 1,
age: 30,
name: name,
};
setUsers([...users, newUser]);
};
const deleteUserHandler = (id) => {
const newUserList = users.filter((user) => user.id !== id);
setUsers(newUserList);
};
return (
<div className="app-container">
<input
placeholder="이름을 입력해주세요"
value={name}
onChange={(e) => setName(e.target.value)}
/>
{users.map((user) => {
return <User user={user} key={user.id} handleDelete={deleteUserHandler}/>;
})}
<Button onClick={addUserHandler}>추가하기</Button>
</div>
);
};
export default App;
코드를 작성하다가 어느 순간 코드가 복잡하다고 느껴진다면 관심사의 분리가 필요한 순간인지 생각해보는 게 좋음
리액트에서도 마찬가지로 하나의 컴포넌트에서 모든 기능을 구현할 수 없기 때문에 여러 개의 컴포넌트를 만들어서 조립함
▶ export와 export default 차이점
둘다 모듈을 내보내고 불러오는 방법
모듈은 크게 두 종류로 나뉨
- 복수의 함수가 있는 라이브러리 형태의 모듈
- 개체 하나만 선언되어있는 모듈
대개는 두 번째 방식으로 모듈을 만드는 걸 선호하기 때문에 함수, 클래스, 변수 등의 개체는 전용 모듈 안에 구현됨
그런데 이렇게 모듈을 만들다 보면 자연스레 파일 개수가 많아질 수밖에 없음
그렇더라도 모듈 이름을 잘 지어주고 폴더에 파일을 잘 나눠 프로젝트를 구성하면 코드 탐색이 어렵지 않으므로 이는 전혀 문제가 되지 않음
모듈은 export default라는 특별한 문법을 지원함. export default를 사용하면 '해당 모듈엔 개체가 하나만 있다’는 사실을 명확히 나타낼 수 있음. 내보내고자 하는 개체 앞에 export default를 붙이면 됨
// App.js
function App() {
return <div>My App</div>;
}
export default App;
// Sample.js
export function Sample1 () {
//
}
export function Sample2 () {
//
}
파일 하나엔 대개 export default가 하나만 있음
이렇게 default를 붙여서 모듈을 내보내면 중괄호 { } 없이 모듈을 가져올 수 있음
반면 단순 export로 선언한 모듈은 중괄호 { }를 반드시 사용해야 함
'항해99' 카테고리의 다른 글
리액트 숙련주차 2 (1) | 2023.06.27 |
---|---|
리액트 숙련주차 1 (0) | 2023.06.27 |
리액트 입문주차 2 (0) | 2023.06.23 |
리액트 입문주차 1 (0) | 2023.06.23 |
JavaScript 문법 종합반 4주차 (0) | 2023.06.20 |
댓글