컴포넌트란 무엇이고 왜 리액트에서 중요한 개념일까 ?
모든 사용자 인터페이스는 모두 컴포넌트로 구성되어있다.
예를들어, 같은 아이템인데 다른 데이터 값을 가지고 있다면 컴포넌트로 구성하는 것이 바람직하다.
재사용할 수 있는 빌딩블럭이라는 뜻.
결국 컴포넌트는 HTML / CSS 그리고 자바스크립트의 결합이다.
재사용이 가능하고 유지보수가 쉽다는점이 함수와 비슷하다.
각각의 컴포넌트가 하나의 명확한 기능에 초점을 맞출 수 있도록 구현하는 것이다.
리액트에서는 항상 원하는 최종 상태, 목표상태 또는 다양한 상황에 따른 다른 목표 상태를 정의한다.
그리고 리액트는 실제 웹 페이지에서 어떤 요소가 추가되고, 삭제되고, 업데이트 되어야 하는지를 해결한다.
자신만의 html 태그를 만들고, 그를 정의하여 재사용 하는 데 용이하게 한다.
npx create-react-app 작명 옵션
npm start
localhost:3000 에서 확인할 수 있다.
옵션에 typescript 같은 속성을 추가하면 typescript 로 리액트 프로젝트를 만들 수 있다.
npx 는 해당 프로그램을 임시로 설치하고 지우는 작업이므로
항상 최신버전을 유지할 수 있고 저장공간을 절약할 수 있다.
react 환경에서 다루는 HTML 파일은 기본적으로 public 폴더의 index.html 하나이다.
index.html 에는
<div id="root"></div>
body태그 안에 root 라는 id를 가진 div 하나를 볼 수 있는데,
index.js 파일에
const root = ReactDOM.createRoot(document.getElementsById)('root'));
root.render(<App />)
메인 리액트 app이 렌더링 되는 위치를 지정해준 것인데,
render( 는 root라는 id를 가진 요소의 자리에 렌더링 할 컴포넌트 라는 의미가 된다.
App.js 파일을 살펴보면,
function App() {
return (
<div className="App">
</div>
);
}
export default App;
이런식으로 구성된 함수가 있는데,
JSX 형식으로 구성된 return 을 export default 로 반환한다.
JSX 는 React 팀에서 구성한 특별한 자바스크립트 구문인데,
자바스크립트 파일에서 HTML 형식의 구문을 사용할 수 있게 해주는 역할을 한다.
리액트 프로젝트를 크롬개발자도구 → sources 탭에서 확인해보면
코드에디터에서 작성한 파일이 조금 달라진 것을 확인할 수 있는데,
JSX방식으로 작성한 코드들이 React, React-Dom 기능의 역할로 변환되는 것을 볼 수 있다.
브라우저에서는 JSX방식으로 작성된 코드를 읽을 수 없으므로
라이브러리 기능으로 자동변환 과정을 거쳐 번역해주는 작업이다.
예를들어 바닐라 자바스크립트 에서는 버튼요소를 dom에 추가할 때,
const par = document.createElement('p')
par.textContent = 'Hello World'
document.getElementById('root').appendChild(par)
이런식으로 ‘명령형’ 으로 작성한다.
위 코드는 간단한 p 태그를 추가하는 로직이지만,
조금 더 복잡하고 유기적으로 데이터의 변환이 일어나는 로직이라면
코드가 훨씬 길어질 것이다.
하지만 리액트에서는
function App(){
return (
<p> Hello Wolrd </p>
)
}
이런식으로 간단하게 작성할 수 있다.
리액트는 이 방법에서 크게 벗어나지 않는다.
작은 단위로 설계한 컴포넌트들에 조금 더 편리한 특정 Hook 들을 이용해 제어하는 것이다.
컴포넌트를 분리하는 방식으로 연습하는 것이 좋다.
현업에서 컴포넌트 단위로 분리한 js 파일들을 여러개 만드는 것이 지극히 정상적이기 때문이다.
src 디렉토리에 components 디렉토리를 생성하고
그 안에 여러 컴포넌트들을 설계할 것이다.
이는 App.js를 ‘루트 컴포넌트’ 로 두고 여러 컴포넌트들을 App 컴포넌트에 병합하는 과정인데,
재사용과 유지보수의 관점에서 유리하다.
컴포넌트 트리 라고 하는데
이런식으로 A 라는 루트 노드 (리액트의 경우 컴포넌트) 에
하위 노드 (하위 컴포넌트) 를 추가하는 방식이다.
리액트의 컴포넌트는 다른게 아니라 하나의 함수일뿐이다.
components 폴더에 ExpenseItem.js 라는 파일을 만들고
const ExpenseItem = () => {
return <h2>Expense Item !</h2>;
};
export default ExpenseItem;
함수를 설계한다.
ExpenseItem 이라는 함수는 <h2>
태그를 리턴하는 함수이다.
해당 함수를 다른 파일에서 쓰기위해서 export 해주는 과정
App.js에서
import ExpenseItem from './components/ExpenseItem';
function App(){
return (
<ExpenseItem />
)
}
import 하면 해당 함수를 마치 HTML 태그처럼 사용할 수 있다.
컴포넌트는 내장 HTML와 구분하기 위해 대문자로 시작하는 것이 원칙이다.
const ExpenseItem = () => {
return (
<div>Date</div>
<div>
<h2>title</h2>
</div>
);
};
JSX 는 중요한 규칙이 있는데,
return 하는 요소는 단 하나의 루트 요소를 갖는다는 것이다.
2개 이상의 div를 허용하지 않는 것이다.
이를 해결하기 위해서, 큰 div 로 묶는 방법이 있다. 이 방법이 제일 간단하다.
리액트에서도 CSS 를 사용한다.
CSS 파일을 생성하고,
HTML 태그에 class 를 추가해, 각 태그의 속성을 선택하여 스타일링을 하던 방식 그대로
JSX 에서도 할 수 있다.
const ExpenseItem = () => {
return (
<div className="items">
<div className="item-wrapper">
<h2 className="item-title">Car</h2>
<div className="item-price">$294.64</div>
</div>
</div>
);
};
JSX 에서는 class 가 className 라는 키워드로 바꾸어 사용해야 한다.
위에서 export 한 css 파일을 스타일링 하면 스타일을 적용시킬 수 있다.
여러개의 데이터를 받아 동적으로 원하는 태그에 할당하려면 어떻게 해야하는지 알아보자.
const ExpenseItem = () => {
const expenseDate = new Date()
const expenseTitle = 'Car'
const expensePrice = '294.64'
return (
<div className="items">
<div className="item-wrapper">
<h2 className="item-title">Car</h2>
<div className="item-price">$294.64</div>
</div>
</div>
);
};
해당 함수를 return 하기 전에 자바스크립트 코드를 작성할 수 있다.
함수 내에 선언한 변수를
const ExpenseItem = () => {
const expenseDate = new Date()
const expenseTitle = 'Car'
const expensePrice = '294.64'
return (
<div className="items">
<div>{expenseDate}</div>
<div className="item-wrapper">
<h2 className="item-title">{expenseTitle}</h2>
<div className="item-price">{expensePrice}</div>
</div>
</div>
);
};
중괄호로 JSX return 내에 동적으로 할당해줄 수 있다.
그런데 위의 코드를 실행시키면 제대로 작동하지 않는데,
그 이유는 new Date() 는 객체형태의 데이터라서 그렇다.
<div>{expenseDate.toDateString()}</div>
Date 의 메소드 toDateString()나 toISOString() 등을 사용하면 제대로 출력된다.
위에서 만든 컴포넌트는 재사용이 불가능하다.
데이터를 변수에 담아 할당하긴 했지만 여전히 데이터가 하드코딩 되어있기 때문이다.
함수에는 재사용을 위해 파라미터를 사용해 구멍을 뚫고 그 구멍에 원하는 데이터를 알맞게 가공할 수 있다.
리액트의 컴포넌트에서도 가능하다.
const ExpenseItem = (props) => {
return (
<div className="items">
<div>{props.item}</div>
<div className="item-wrapper">
<h2 className="item-title">{props.title}</h2>
<div className="item-price">{props.price}</div>
</div>
</div>
);};
재사용 할 컴포넌트에서 props를 정의해준다.
마치 함수에 파라미터를 부여하는 것과 같은데, 해당 props 키워드는
원하는 대로 작명해도 상관없다.
객체에서 자료를 뽑아오는 것 처럼 props.키워드 로 원하는 자료를 바인딩한다.
그 후 루트 컴포넌트에서
const items = {
item : 'item A',
title : 'title A',
price : 10000
}
...
<ExpenseItem
item={items[0].item}
title={items[0].title}
price={items[0].price}
/>
props에 바인딩할 key 값을 정의해주면 된다.
이전에 컴포넌트 트리 구조를 봤다.
컴포넌트내의 코드가 길어져 세분화 하고싶을 경우가 있을 수 있는데,
이런 경우 컴포넌트의 자식 노드를 구성하면 된다.
컴포넌트의 자식을 만드는 규칙은 없다. 자유롭게 세분화 할 수 있는데,
강의의 저자는 기능을 담당하는 div 단위로 구분하는 듯 하다.
컴포넌트를 분할하는 일은 어렵지 않다.
App 컴포넌트에서 자식노드를 생성할 때 처럼,
컴포넌트에서 자식 컴포넌트를 import 해오고
컴포넌트 이름을 불러주면 되는 것이다.
// App.js
...
<ExpenseItem
title={expenses[0].title}
amount={expenses[0].amount}
date={expenses[0].date}
/>
...
// ExpenseItem 컴포넌트에 expense 객체의 데이터를 명시된 키에 담아 전송
// ExpenseItem.js (자식 컴포넌트)
return (
<div className="expense-item">
<ExpenseDate date={props.date} /> // <- 자식 컴포넌트의 세분화
<div className="expense-item__description">
<h2>{props.title}</h2>
<div className="expense-item__price">$ {props.amount}</div>
</div>
</div>
);
// 전달받은 객체의 데이터를 props 로 받아 사용, 자식 컴포넌트로 명시된 키에 담아 전송
// ExpenseDate.js (자식 컴포넌트의 자식 컴포넌트)
const ExpenseDate = (props) => {
const month = props.date.toLocaleString("ko", { month: "long" });
const day = props.date.toLocaleString("ko", { day: "2-digit" });
const year = props.date.getFullYear();
return (
<div className="expense-date">
<div className="expense-date__year">{year}</div>
<div className="expense-date__month">{month}</div>
<div className="expense-date__day">{day}</div>
</div>
);
};
// 부모 컴포넌트로 부터 받은 props를 이용해 정보를 가공하고 변수에 담아 태그를 리턴
위 코드를 보면 props 로 받아온 데이터를 다시 props 로 전달해주는 것을 볼 수 있다.
지금 단계에서는 이 props 의 중첩을 대체할 다른 방법은 없다.
나중에 Redux 같은 상태관리 라이브러리로 이를 해소할 수 있다고 한다.
컴포넌트를 또 분리해보자.
App.js 파일에 Expense 를 여러개 그리는 과정을 하나의 컴포넌트로 묶어볼 수 있다.
컴포넌트는 세분화 하는 방법도 있지만 여러개의 컴포넌트를 하나로 묶을수도 있다.
// App.js
return (
<div className="App">
<Expense items={expenses} />
</div>
);
// expense 데이터를 새로 만든 컴포넌트 Expense 에 props로 할당. key는 items
// Expense.js
const Expense = (props) => {
const expenses = props.items.map((e, n) => (
<ExpenseItem key={n} title={e.title} amount={e.amount} date={e.date} />
));
return <div className="expenses">{expenses}</div>;
};
전달받은 props 를 콘솔에 찍어보면 items 라는 key를 가진 객체가 나온다.
해당 객체의 items 는 객체를 원소로 갖는 배열임으로 map 반복문으로 해당 배열의 갯수만큼
HTML 태그를 생성하는 로직이다.
JSX 에서는 기본적인 for 반복문을 쓸 수 없다.
따라서 map 같은 배열 메소드를 이용해
const Expense = (props) => {
const expenses = props.items.map((e, n) => (
<ExpenseItem key={n} title={e.title} amount={e.amount} date={e.date} />
));
return <div className="expenses">{expenses}</div>;
};
이런식으로 작성해주었다.
이 부분에서 key 라는 값을 바인딩 해주는 이유는
Each child in a list should have a unique "key" prop.
이러한 오류가 출력되기 때문이다.
해석하자면 많은 요소들이 식별가능한 key 를 가지고 있지 않다는 뜻인데,
이러한 배열의 key는 고유성을 부여해주는 역할을 한다.
생성되는 요소에 고유성을 부여해주기 위해 key를 내부에 지정해주는 것이다.
위의 경우에는 index로 key를 할당해주었지만,
고유성을 식별할 수 있는 수단이라면 어느것이어도 상관없다.
보통 데이터의 ID를 key로 사용하는것이 일반적이라고 한다.