[Foundation] From JS to React
1. UI 업데이트
1.1 UI 렌더링
리액트가 어떻게 동작하는지에 앞서 브라우저가 어떻게 코드를 해석하고 UI를 그려내는지 이해해야 한다. 기본적으로 어떤 웹 페이지를 들어가게 되면 서버는 HTML 파일을 리턴하는데, 대강 아래와 같은 모양새를 띤다. 그 후 브라우저는 서버로부터 전달받은 HTML 파일을 읽어 DOM을 그려내게 된다.
1.2 DOM?
DOMThe Document Object Model은 쉽게 말해 HTML 문서를 객체 형태로 표현한 것이다 (자세한 의미는 여기를 참고하자) DOM은 트리 구조로 부모-자식 관계를 갖는 모양새를 하고 있는데, 코드와 UI 사이에서 일종의 브릿지 역할을 담당한다.
이게 무슨 말인가 싶을 텐데, 개발자가 DOM 메서드와 JS 같은 언어를 통해 유저의 동작을 인식하고(클릭, 드래그 등등의 user event) UI변화를 위해 DOM을 조작할 수 있다는 뜻이다. 즉, DOM을 조작함으로써 페이지의 내용이나 스타일에 변화를 줄 수 있다는 뜻.
1.3 DOM 메서드
그럼 실제로 DOM 메서드로 UI를 업데이트해 보자. <div> 태그에 <h1> 태그를 추가하는 동작을 해볼 건데, 이 과정에서 아래 DOM 메서드들을 써볼 것이다.
- getElementbyId
- createElement
- appendChild
<!-- index.html -->
<html>
<body>
<div id="app"></div>
<script type="text/javascript">
// Select the div element with 'app' id
const app = document.getElementById('app');
// Create a new H1 element
const header = document.createElement('h1');
// Create a new text node for the H1 element
const headerContent = document.createTextNode(
'Develop. Preview. Ship. 🚀',
);
// Append the text to the H1 element
header.appendChild(headerContent);
// Place the H1 element inside the div
app.appendChild(header);
</script>
</body>
</html>
위 코드를 실제로 돌려보면 아래와 같은 결과가 나온다.
1.4 HTML vs DOM
이제 브라우저의 개발자 도구를 켜서 페이지를 뜯어보면 DOM에 <h1> 엘레멘트가 포함된 것을 확인할 수 있을 것이다. 자세히 보면 원본 HTML 파일과 DOM의 모습이 다른 것을 확인할 수 있는데, 이는 HTML은 초기 페이지 컨텐츠만 표현하는 반면, DOM은 업데이트된 페이지 컨텐츠를 반영하기 때문이다.
이처럼 DOM을 조작함으로써 UI를 업데이트할 수 있는데, 물론 이 자체만으로도 훌륭하지만 바닐라 JS로 DOM을 조작하는 방식은 매우 장황하고 번거롭다는 게 문제이다. 실제로 <h1> 태그에 텍스트 몇 글자 좀 채워서 껴넣으려했을 뿐인데 코드량이 쓸데없이 많지 않았는가?
<script type="text/javascript">
const app = document.getElementById('app');
const header = document.createElement('h1');
const headerContent = document.createTextNode(
'Develop. Preview. Ship. 🚀',
);
header.appendChild(headerContent);
app.appendChild(header);
</script>
만약 애플리케이션 사이즈가 좀 더 커지거나, 대규모 팀에서 이런 식으로 개발해야 한다면 그야말로 극한직업이 따로 없을 것이다.
1.5 프로그래밍 : 명령형 vs 선언형
위 코드 방식을 명령형 프로그래밍이라고 하는데, 쉽게 말해 개발에 필요한 방법을 구구절절 단계별로 설명하는 방식이다. 실제로 UI 업데이트가 어떻게 이뤄져야 하는지 구구절절 단계별로(createElement → createTextNode → appendChild) 작성해 줬었다.
이 방식은 시간이 오래 걸리고 장황하기 때문에 실제 웹개발에선 선언형 프로그래밍 방식을 많이들 선호하는데, DOM메서드를 구구절절 작성할 필요 없이 필요한 내용만 선언해 나가는 방식이다.
그리고 이 선언형 라이브러리 중 하나로 바로 React가 있는 것.
2. React 시작하기
React는 선언형 UI 라이브러리의 일종으로 개발하는 방식은 개발자가 UI에 하고 싶은 짓을 선언하기만 하면 React가 단계별로 알아서 DOM 업데이트를 수행한다.
2.1 리액트 적용
프로젝트에 리액트를 사용하기 위해 두 가지 리액트 스크립트를 추가해 보자.
- react : 리액트 코어 라이브러리
- react-dom : DOM과 함께 리액트를 사용할 수 있게 DOM 메서드를 제공
리액트 추가가 끝났으면 바닐라 JS를 대신해 리액트로 DOM을 조작해 볼 건데, 여기서 <h1> 엘레멘트를 그리기 위해 react-dom의 ReactDOM.render메서드를 사용할 것이다.
<html>
<body>
<div id="app"></div>
<script src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<script type="text/javascript">
const app = document.getElementById('app');
ReactDOM.render(<h1>'Develop. Preview. Ship. 🚀'</h1>, app);
</script>
</body>
</html>
그러나 위 코드를 실행해 본들 에러만 내뿜으며 제대로 돌아가질 않는데, 그 이유는 <h1>...</h1>이 JS의 올바른 문법이 아니기 때문이다. 이게 뭔 소린가 싶을 텐데, 사실 <h1>...</h1>은 JSX 코드일 뿐 JS의 코드가 아니다.
2.2 JSX
JSX란 자바스크립트의 확장 문법으로, 자바스크립트로 UI를 그릴 때 HTML로 쓰는 것마냥 사용할 수 있게 해주는 문법이다. 이 덕분에 별도로 뭔가를 배울 필요 없이 평소에 쓰던 HTML과 JS문법을 그대로 쓸 수 있게 된다.
단, JSX가 온전히 JS문법 그 자체인건 아니므로 브라우저 입장에선 이를 이해할 수 없고 때문에 Babel같은 컴파일러를 사용해 JSX코드를 JS코드로 변환해 주는 작업이 필요하다.
2.3 바벨 추가하기
그래서 결론은 위의 안 돌아가던 코드에 Babel을 추가해 주면 정상적으로 작동하게 된다는 것. 단, script태그의 type을 text/babel로 설정해줘야 하는데, 그 이유는 바벨에게 어떤 코드를 변환시킬지 알려주기 위함이다.
<html>
<body>
<div id="app"></div>
<script src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
// script의 type을 text/babel로 설정해줘야 한다
<script type="text/babel">
const app = document.getElementById('app');
ReactDOM.render(<h1>'Develop. Preview. Ship. 🚀'</h1>, app);
</script>
</body>
</html>
결과물을 보면 확실히 리액트를 사용함으로써 반복적인 코드를 많이 쳐낼 수 있었다. 이처럼 리액트는 재사용 가능한 코드 스니펫을 포함한 라이브러리로, 개발자를 대신해 많은 작업들을 대신 처리해 준다.
3. 컴포넌트
리액트의 코어 컨셉으로 세 가지가 있는데, 바로 Components, Props, State이다. 앞으로 차근차근 알아볼 건데 먼저 컴포넌트에 대해 알아보자.
3.1 컴포넌트로 UI 만들기
UI는 좀 더 작은 조각들로 쪼갤 수 있는데, 이 조각들을 컴포넌트component라고 한다.
컴포넌트는 레고 블록과 비슷한 개념이라 개별 블록들을 조합함으로써 더 큰 구조를 구축할 수 있다. 덕분에 독립적이고 재사용 가능한 코드 스니펫을 만들 수 있는데, 더 훌륭한 건 UI의 업데이트가 필요한 순간엔 특정 부분의 컴포넌트만 다른 걸로 갈아 끼워주면 된다는 것이다!
이러한 모듈성 덕분에 애플리케이션의 나머지 부분을 건드리지 않고도 컴포넌트를 쉽게 추가, 업데이트, 삭제할 수 있고, 결과적으론 코드의 유지 관리가 더욱 쉬워진다.
그리고 React 컴포넌트의 나이스한 점은 그냥 자바스크립트일 뿐이라는 점이다. 여하튼 자바스크립트 관점에서 React 컴포넌트를 작성하는 방법을 살펴보자.
3.2 컴포넌트 만들기
앞서 컴포넌트가 조각(블록) 같은 개념이라 얘기했는데, 정확히는 UI 요소를 리턴하는 함수를 말한다. 컴포넌트 함수는 JSX로 작성된 UI 요소를 리턴하며, ReactDOM.render 메서드의 첫 번째 인수로 전달해 화면상에 UI를 그려낼 수 있다.
<html>
<body>
<div id="app"></div>
// ... 중략
<script type="text/babel">
const app = document.getElementById('app');
function Header() {
return <h1>'Develop. Preview. Ship. 🚀'</h1>;
}
ReactDOM.render(<Header />, app);
</script>
</body>
</html>
컴포넌트를 작성할 땐 몇 가지 따라야 할 규칙이 있는데, 하나는 첫 글자를 대문자로 작성해야 한다는 것이고 다른 하나는 HTML태그와 마찬가지로 <>로 감싸줘야 한다는 것이다.
3.3 Nesting 컴포넌트
당연한 소리지만 애플리케이션은 통상 단일 컴포넌트로 이뤄지지 않는다. 대신 여러 컴포넌트를 쓰게 되는데, 평범한 HTML과 동일하게 컴포넌트도 중첩이 가능하다.
아래 예시는 Homepage라는 컴포넌트에 Header라는 컴포넌트를 중첩시키는 예시이다.
function Header() {
return <h1>Develop. Preview. Ship. 🚀</h1>;
}
function HomePage() {
return (
<div>
{/* Nesting the Header component */}
<Header />
</div>
);
}
ReactDOM.render(<Header />, app);
3.4 컴포넌트 트리
위에서 본 컴포넌트 중첩을 활용해서 결국 트리 형태의 컴포넌트 구조를 만들 수 있는데, 가령 아래 그림과 같은 모양새가 될 것이다.
예를 들어, 최상위 HomePage 컴포넌트엔 Header, Article, Footer 컴포넌트가 네스팅 되어있고, 그 안의 Header 컴포넌트엔 Logo, Title, Nav 같은 하위 컴포넌트들을 또다시 네스팅되어 있는 형태로 말이다.
이러한 모듈 형식을 사용함으로써 애플리케이션 내 여러 위치에서 컴포넌트를 재사용할 수 있게 된다. 앞서 리액트를 '재사용 가능한 코드 스니펫을 포함한 라이브러리'라고 언급한 바 있는데, 바로 이 부분이 '재사용 가능한 코드'를 뒷받침하는 것들 중 하나이다.
4. Props
컴포넌트로 UI를 그리는 법을 배웠는데, 막상 만들고보니 똑같은 내용으로만 재사용이 가능했다. <Header />컴포넌트를 얼마나 갖다쓰건 'Develop. Preview. Ship. 🚀'라는 글자만 계속 나타나므로 실상은 재사용의 의미가 없는 것이다.
그렇다면 만약 다른 텍스트를 보여주거나 다른 데이터를 보여주고싶다면 어떻게 해야할까?
4.1 Props란?
위 물음에 대한 답이 바로 Props이다. 함수가 인자(프로퍼티)를 받아서 특정 결과를 리턴하듯, 컴포넌트도 프로퍼티를 전달받아 화면상에 렌더될 때 특정 동작을 수행하거나 보여지는 내용물을 바꿀 수 있다.
이처럼 리액트 컴포넌트에 프로퍼티로써 특정 정보를 전달할 수 있는데, 이를 props라고 한다.
NOTE
리액트에서 데이터는 컴포넌트 트리상 하방으로 흘러간다(즉, 부모 컴포넌트 → 자식 컴포넌트 방향으로)
4.2 Props 활용하기
실습을 위해 HomePage컴포넌트에서 title이란 이름으로 prop을 전달해보자. 자식 컴포넌트인 Header는 함수의 인자로써 이 props를 전달받는데, 콘솔창에 찍어보면 객체 형태로 나타나는 모습을 볼 수 있다.
function Header() {
console.log(props) // { title: "React 💙" }
return <h1>title</h1> // title이 그대로 나옴
}
function HomePage() {
return (
<div>
<Header title="React 💙" />
</div>
);
}
이처럼 props는 객체 형태로 전달되기 때문에 디스트럭처링 문법도 사용이 가능하다.
그런데 문제가 있는데, title이 찍혀나오긴 하지만 'React 💙'라는 글자가 아니라 정말 말 그대로 'title'이란 글자가 찍혀나오고 있다. 이러한 현상이 발생하는 이유는 리액트가 title을 어떤 변수가 아니라 평범한 텍스트로 인식했기 대문이다. 따라서 리액트에게 title은 스트링이 아니라 변수라고 알려줄 방법이 필요하다.
4.3 JSX에서 Variable사용하기
변수를 사용하기 위해선 해당 변수를 { } (curly braces)로 감싸줘야 한다. JSX 문법중 하나인데, 이렇게 감싸고나면 JSX 내부에서 자바스크립트 변수마냥 곧바로 사용할 수 있다.
JSX에서 중괄호로 감싸진 부분은 사실상 자바스크립트와 다를 바 없어서 JS 표현식을 그대로 갖다쓸 수 있다. 그 예시를 아래 코드상에 나열해놨으니 구경해보길 바란다.
function Header(props) {
// dot notation
return <h1>{props.title}</h1>;
// template literal
return <h1>{`Cool ${title}`}</h1>;
// returned value of a function
return <h1>{createTitle(title)}</h1>;
// ternary operators.
return <h1>{title ? title : 'Default Title'}</h1>;
}
function createTitle(title) {
if (title) return title;
else {
return 'Default title';
}
}
4.4 list를 활용한 반복문
애플리케이션을 만들다보면 리스트 형태로 데이터를 보여줄 일이 많은데, 이 경우 반복적인 UI 요소를 생성하기 위해 Array 메서드를 쓸 수 있다.
NOTE
리액트의 데이터 패칭과 관련해선 정해진 정답같은게 그닥 없는지라 본인의 요구사항에 잘 맞는 솔루션을 선택하면 된다. 물론 Next.js에서도 데이터 패칭과 관련한 옵션들을 다룰 예정이지만, 여하튼 지금은 그냥 간단한 배열로 데이터를 표현하기로 하자.
구체적으론 Array.prototype.map 메서드를 쓸 수 있는데, 중괄호 안에서 화살표 함수를 써서 아래와 같이 작성할 수 있다.
function HomePage() {
const names = ['Ada Lovelace', 'Grace Hopper', 'Margaret Hamilton'];
return (
<div>
<Header title="Develop. Preview. Ship. 🚀" />
<ul>
{names.map((name) => (<li>{name}</li>))}
</ul>
</div>
);
}
그런데 위 코드를 실행하면 missing key prop이란 워닝 메시지가 뜨게 된다. 그 이유는 리액트가 배열의 각 요소를 구분하기위해 unique key를 필요로 하기 때문이다. 물론, key가 없어도 동작하기는 하지만 리액트는 key를 통해 DOM을 보다 효율적으로 업데이트하므로 잊지 말고 key값을 정해주자.
names.map((name) => (<li key={name}>{name}</li>))
다시 한번 강조하지만 key값은 unique해야 한다는걸 잊지 말자.
5. States