강의에 덧붙여 제가 이해한 내용을 같이 첨부하였습니다.
2편의 목차는 다음과 같이 구분하였습니다.
1. 컴포넌트란?
2. props란?
리액트는 사용자 정의 태그(컴포넌트)를 만드는 기술이다.
저번 시간에 리액트를 다음과 같이 정의했다. 컴포넌트를 만들든, 컴포넌트를 가지고 UI를 만들든 그게 그거다.
그러나 컴포넌트에 대해서는 완벽히 이해를 하지 못하고 넘어갔다.
이번 시간에는 IT계의 명언인 '백문이 불여일타'를 생각하며, 실제 작동하는 컴포넌트를 만들어 보면서 컴포넌트에 대한 이해를 해보자.
1. 컴포넌트란?
>app.js
function App() {
return (
<div>
<h1>Counter</h1>
<button>+</button> 0
</div>
);
}
골격이지만, 간단한 카운터를 하나 만들어 주었다.
카운터임을 알려주기 위해 h1태그로 카운터임을 알려 주었고,
button 태그로 더하기 버튼을 만들어주고,
그 뒤에는 하드코딩이지만 숫자를 집어넣어 주었다.
그런데 내가 카운터를 만약 1억개 만들고 싶다면, 저 코드블럭에 있는 내용들을 몽땅 다 copy해서 1억개를 paste해야 한다.
만약 이 카운터가 1억줄 짜리라면 전체 코드 줄수는 10,000,000,000,000,000줄로, 공간을 엄청나게 낭비하는 비효율적인 코드가 탄생할 것이다.
또, 이 1억개의 카운터를 만들었는데 문제가 발생한다면?
1억개의 카운터를 몽땅 손봐줘야 할 것이다.
아마 개발자를 지향하는 사람들은 나처럼 '사회의 귀찮음을 해결하기 위해' 개발자가 되기로 결심한 사람이 많을 것이므로,
조금 더 똑똑하게 코드를 만들어 보자.
어떻게?
바로 이 카운터를 '사용자 정의 태그'로 만들어 주는 것이다.
Counter라는 이름의 사용자 정의 함수를 만들어 보자.
> app.js
function Counter(){
return <div>
<h1>Counter</h1>
<button>+</button> 0
</div>
}
여기서 <div> </div>의 wrapping tag를 빼면 오류가 발생한다.
왜 에러가 발생할까?
리액트에서는 컴포넌트를 만드는 함수를 정의할 때에는
`반드시 단 하나의 단일 태그만을 리턴`해야 한다.
다시 돌아가서, 이제 app.js에서 <Counter></Counter>를 입력하면, 우리가 만들었던 카운터가 그대로 나오게 된다.
이제 이 카운터를 1억개 넣어도 코드가 1억줄만 나오게 되어 매우 간결(?) 하며, 오류 수정을 하기도 쉽다.
또, 컴포넌트의 이름과 내용을 매칭시키면서 이 태그가 어떤 태그인지 다른 사람이 확인하기도 좋다.
정리
1. 리액트는 `사용자 정의 태그`를 만드는 기술이다.
2. 사용자 정의 태그를 '컴포넌트'라고 한다.
3. 컴포넌트는 함수다.
4. 함수의 리턴값이 컴포넌트의 UI값이 된다.
5. 컴포넌트의 이름은 반드시 `대문자`여야 한다.
6. 리턴값은 반드시 '하나의 태그'로 시작해야 한다.
2. props란?
우리가 만든 컴포넌트를 조금만 더 바꿔 보자.
카운터를 1억개 쓰는데 모조리 다 똑같은 카운터라면, 존재의 의미가 있을까?
그래서 이제 <Counter title="xxx 카운터"></Counter>에 있는 title대로 렌더링을 하도록 바꿔보자.
일단 다음과 같이 컴포넌트를 바꾸고, 콘솔에 찍어보자.
function Counter(props){
console.log(props)
return (
<div>
<h1>카운터</h1>
<button>+</button>0
</div>
)
}
function App(){
return (
<div>
<Counter title ="xxx 카운터"></Counter>
</div>
)
}
콘솔에 다음과 같이 {title : 'xxx 카운터'}가 찍히는 것을 확인할 수 있다.
따라서 '카운터'라는 글자를 {props.title}로 변경하면 재렌더링이 된다.
{} 안에 점을 찍으면 '~의' 라는 뜻을 가진다.
이렇게 props에 여러 속성들을 줄 수도 있다. 이번엔 카운팅 초기값을 한 번 줘보자.
function Counter(props){
console.log(props.title)
return(
<div>
<h1>{props.title}</h1>
<button>+</button>{props.initValue}
</div>
)
}
export default function App() {
return (
<div>
<Counter title = "불면증 카운터", initValue = {10}></Counter>
</div>
);
}
initValue = "10"으로 해 준 사람들도 있을 수 있겠으나, 이렇게 하면 안된다.
"10"으로 할 시 리액트에서는 이 안의 문자를 스트링으로 인식하기 때문에,
중괄호 안에 숫자를 넣어서 타입을 number로 설정해 주어야 한다.
이제 우리는 깨달았다.
컴포넌트의 속성(title, initValue)을 적으면, 컴포넌트 함수(Counter)의 첫 번째 파라미터로(Counter()) 객체가 주입되는데, 그 객체의 프로퍼티 값(props)으로 속성의 값(title, initValue)이 전달된다는 것을.
그런데 갑자기 궁금한 점, 콘솔에는 왜 값이 2번씩 찍힐까?
> index.js
const rootElement = document.getElementById('root');
const root = createRoot(rootElement);
root.render(
<StrictMode>
<App />
</StrictMode>
);
React.StrictMode는 애플리케이션 내의 잠재적인 문제를 알아내기 위한 도구로, 이걸 켜면 2번씩 실행된다. UI를 재렌더링 하지 않는다. 그러나 어짜피 build하면 한번만 실행된다.
이왕 index.js 파일을 열어본 김에, 그 위의 rootElement를 보면, html에서 id가 root인 태그 안에 우리의 html이 들어간다는 내용을 확인할 수 있다. 각자 콘솔창을 열어서 id=root인 태그가 있는지 확인 해 보자.
그런데, 우리가 만든 root 이외의 영역은 우리 파일 중 어디에 있을까?
> public > index.html
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
이 root 안에 우리가 만든 어플리케이션이 들어가도록 create-react-app이 구현을 해 놓은 것이다.
지금까지 한 내용들을 정리해보자.
1. props는 입력값이다.
2. props에 중괄호를 사용하면 js의 데이터 타입으로 주입되고, 따옴표를 사용하면 문자열로 평가된다.
3. 컴포넌트(함수)의 리턴값에서 중괄호를 사용하면, 변수로 간주된다. (=> {props.title})
지금까지 우리가 만든건 그냥 틀을 만든 것 뿐이고, 이제 실제로 버튼이 동작하게 만들어보자.
<button onClick = {function(){console.log(1)}}>+</button>
버튼을 누르게 되면 콘솔에 1이 찍히는 익명함수를 만들어 주었다. 작동도 잘 한다.
그런데 보기가 너무 불편하므로, 함수를 바깥으로 빼보자.
function up(){
console.log(1);
}
<button onClick = {up}>+</button>{props.initValue}
이제는, up 함수가 실행될때마다 초기값을 하나씩 올려보자.
function up(){
props.initValue = props.initValue + 1;
}
<button onClick = {up}>+</button>{props.initValue}
과연 정상작동이 될까? 결과는 런타임 에러를 뱉는다.
리액트에서, 컴포넌트 안에서는 컴포넌트 바깥의 값을 바꾸면 안되기 때문이다.
그러면, 다음과 같이 바꾸어 보자.
function Counter(props){
let count = props.initValue;
function up(){
console.log(1);
count = count + 1;
}
return ( ... )
}
이 코드는 실행될까?
에러는 나지 않지만, 컴포넌트가 re-rendering 되지는 않는다.
사실 리액트에서는 함수가 다시 실행되지도 않았는데 변수인 count값이 변한다고 그냥 리렌더링을 발생시킬 이유가 없다.
그래서, 이제 지역변수 count를 '상태'로 승진시켜야 한다.
import { useState } from 'react';
function Counter(props){
let countState = useState(props.initValue);
console.log(countState)
function up(){
//count = count + 1;
}
return (...)
}
이 useState라는 친구로 10을 기본값으로 갖는, '상태'를 만들어 주었다. 이름도 상태에 맞게 countState로 바꿔주었다.
추가로, use~ 이런 함수들을 훅(Hook)이라고 한다.
그런데 이 useState의 형태는 어떻게 생겼을까? count의 로직을 주석 처리하고, countState를 콘솔에 찍으면 다음과 같이 나오게 된다.
콘솔에 찍어보니, 다음 3가지를 알 수 있다.
- '상태'는 '배열'이다.
- '상태'의 첫 번째 원소는 '상태의 값'이다.
- '상태'의 두 번째 원소는 '상태의 값을 바꿀 때 호출하는 함수'다.
그래서 첫 번째 원소는 '읽을 때', 두 번째 원소는 '바꿀 때' 사용한다는 것을 알아차릴 수 있다.
그래서 우리는 countState의 첫 번째 원소를 count로 정의하고, 두 번째 원소를 count를 바꿀 수 있는 setCount 함수로 정의한다.
이해하기 힘든 사람이 있을 수도 있어, 전체 코드와 흐름을 다시 올려본다.
import {useState} from 'react';
function Counter(props){
let countState = useState(props.initValue);
let count = useState(0);
let setCount = useState(1);
function up(){
setCount(count + 1);
}
return (
<div>
<h1>{props.title}</h1>
<button onClick = {up}>+</button>{count}
</div>
)
}
function App(){
return (
<div>
<Counter title = "불면증 카운터", initValue = {10}></Counter>
</div>
)
}
흐름을 글로 정리하자면 다음과 같다.
count를 return값에 배치하고, 이 count를 바꿀 수 있는 setCount라는 함수를 up함수 안에 배치한다.
우리가
1. 버튼을 클릭하면,
2. up함수가 실행되면서,
3. setCount가 count의 값을 바꾸고,
4.리액트는 전체를 바꾸지 않고 이 count 부분만 재렌더링한다.
만약 setCount(count+1)을 count = count + 1로 치환하게 되면 count부분이 재렌더링 되지 않는다.
리액트는 setCount라는 함수를 호출하고, 이 함수 안의 값이 이전과 달라졌을 때만 재렌더링 하기 때문이다.
만약 setCount(count + 1)을 setCount(11)으로 바꿔주면, 값이 불변하기 때문에 절대 재렌더링 되지 않는다.
그리고 이 불필요한 렌더링 방지는 리액트의 성능 향상에 큰 영향을 끼친다.
지금까지 한 것들을 복습해보자.
1. 리액트의 코드는 js가 아니라 jsx이다.
- jsx는 Js+태그를 쉽게 코드에 포함시킬 수 있도록 고안된 js 확장 언어다. onclick -> onClick, onchange -> onChange
2. 이벤트의 값은 함수다.
3. props의 값은 read only(어명), State의 값은 쓰기 가능(지방 방송)
4. state는 배열이다.
- 첫번째 원소는 값이다. 읽을 때 쓰고, 수정하면 안된다.
- 두번째 원소는 함수다. 수정할 때 호출한다.
5. 파라미터가 변경되었을 때만 컴포넌트가 다시 실행된다.
6. props, state가 바뀌면 컴포넌트가 다시 렌더링 된다.
이제 props란 무엇인지, state란 무엇인지, 그리고 왜 쓰이는지에 대해 알게 되었다.
승리의 박수 한번 치고, 다음으로 넘어가보자.
그리고 개인적으로, 스프링과 리액트 둘을 공부하면서, 프로그래밍의 몇 가지 공통점을 알게 되었다.
- 어느 언어를 택하든, 백엔드든 프론트엔드를 하든 '유지보수가 간편한 코드'와 '남이 알아볼 수 있는 코드'가 정말 중요하다는 것.
- '좋은 코드는 무엇인가'에 대한 답은 명확하지 않아도, '나쁜 코드는 무엇인가'에 대한 답은 명확하다.
- 지금 작동하는 코드가 어떤 동작을 하느냐를 메소드/컴포넌트 이름만 보고서도 알수 있는 것이 가장 좋다. 그래서 ~케이스라는 규칙이 등장하고, 변수 명명 규칙등이 생겼다.
- '객체'의 유무 차이는 2D와 3D의 차이다. 그만큼 객체로 인해 많은 것을 얻을 수 있고, 또 많은 것을 조심해야 한다.
- '중복되는 코드'를 기피하는 방법을 찾으려 많은 생각을 하자.
- 변수가 영향을 미치는 범위를 잘 생각해서 코드를 작성하자.
'프론트 > React.js' 카테고리의 다른 글
[생활코딩 밤공부 1기] React(1) - 리액트란 어떤 기술일까? (0) | 2023.07.27 |
---|