
오늘은 생활코딩 이고잉님의 React 강의를 들었다. 그동안 꾸준하게 React를 공부하고 싶다는 생각이 있었는데, 아직 바닐라JS도 부족한 나에게 리액트는 무리가 아닐까 생각하며 반려하고는 했었다.
그리고 실제로 한 달 전인가 이고잉님의 동일한 리액트 강의를 State 부분까지 들었었는데, 당시에는 이해가 안되고 어렵게 느껴져서 "아직은 때가 아닌가보다"라고 생각하며 다시 바닐라JS로 돌아섰었다.
그리고 오늘, 다시 강의들 들어보았다. 이제 내배캠에서도 react 수업이 코앞이고 이제는 어느정도 준비가 되었다고 생각해서 다시 강의를 들어봤는데, 강의를 들으며 느낀 점들을 정리해보고자 한다.
P.S
글 다 쓰고 올라왔는데, 오늘은 스압이 진짜 지린다.
1,2번 단락에서는 리액트 개념에 관한 일반적인 서술이 주를 이룬다면, 3,4번 단락에서는 리액트를 처음 접하며 깨달은 점들과 삽질한 점들이 적혀있으니 만약 나처럼 React를 처음 본다면 3,4번 단락에서 내가 겪은 일들을 언젠가 여러분도 똑같이 겪을 것이라고 생각한다. 그때 생각난다면 보시기를.
특히 난 State가 등장하는 부분부터 꽤 헤맨 것 같다.
React는 기본적으로 node.js 위에서 실행되는 것 같다.

대충 이런식으로 파일이 생겼는데,
index.html, index.js, App.js 이 세 파일이 기본적인 구동 파일이 되는 것 같다.
index.html
<head> ... </head> <body> <div id="root"></div> </body>
우리한테 그나마 익숙한 index.html에 들어가면 이게 전부다. root라는 id를 가진 div 태그 하나.
아마도 우리가 js를 이용해서 동적으로 영화 파일을 불러오거나 댓글창을 불러오는 것처럼 저 안에 동적으로 무언가 생성하는 것 같다. 마치 렌더링처럼.
index.js
import React from 'react'; import ReactDOM from 'react-dom/client'; import './index.css'; import App from './App'; import reportWebVitals from './reportWebVitals'; // const root = ReactDOM.createRoot(document.getElementById('root')); root.render( <React.StrictMode> <App /> </React.StrictMode> );
index.html안에는 어떤 <script>태그도 없어서 어떻게 js랑 연결하나 했는데, ReactDOM이라는 객체의 함수로써 index.html의 root를 가져올 수 있는 것 같다.
아마도 createRoot()라는 메서드는 App()을 렌더링할 DOM 위치를 정해서 root 객체를 생성하는 메서드가 아닐까 싶다.
그렇게 root.render()라는 메서드를 사용하면서 <App />이라는 태그를 인자로 전달하는데 이 <App />태그는 동일한 주소 내에 있는 ./App으로부터 온 것이다.
App.js
import "./App.css"; // function App() { return <div></div> } // export default App;
그렇게 App.js를 찾아가보니 App()이라는 함수가 어떤 html 태그를 반환하고 있었고 export default를 통해 다른 js로 이어지고 있었다. 즉 이게 <App />이다.
결국 index.html에 있는 <div id="root"></div>에서 렌더링되는 것은 App.js에 있는 App()이라는 함수가 반환하는 태그였던 것이다.
import "./App.css"; // const Nav = () => ( <Nav> <h2>이건 네비입니다.</h2> </Nav> ) const Header = () => ( <header> <h2>이건 헤더입니다.</h2> </header> ) // function App() { return <div> <Nav></Nav> <Header></Header> </div> } // export default App;
리액트는 위와 같이 내가 원하는대로 태그를 만들 수 있다. 정확히는 원하는 태그를 반환하는 함수를 만드는 것이다.이제 동일한 유형의 태그를 만들 때는 단순하게 복사 붙여넣기로 새로운 태그를 만들 수 있는 것이다.
function App() { return <div> <Header></Header> <Header></Header> <Header></Header> <Header></Header> </div> } // export default App;
이런 식으로 원하는 태그를 얼마든지 반복해서 만들 수 있다. 이러면 일단 훨씬 편해보이기는 한다.
여기까지는 아직 특별하지 않다. 확실히, 내가 원하는 태그를 조합하거나 사전에 설정하여 사용자 정의 컴포넌트를 만드는 것은 매력적이지만 기존의 방식으로 불가능한 것은 아니다. 가령,
function App() { return <div> <Header></Header> <Header></Header> <Header></Header> <Header></Header> </div> }
위에꺼 대신에
function App() { return <div> <header> <h2>이건 헤더입니다.</h2> </header> <header> <h2>이건 헤더입니다.</h2> </header> <header> <h2>이건 헤더입니다.</h2> </header> <header> <h2>이건 헤더입니다.</h2> </header> </div> }
이렇게 작성한다고 해서 문제가 될 것은 없다. 다만 props를 활용하면서부터 React의 단맛이 본격적으로 느껴진다.
예를 들어서 <header></header>태그마다 header태그 안의 내용을 어떤 변수로 바꾸고 싶다고 가정해보자. 그러면 React에서는 이렇게 할 수 있다.
const Header = (props) => ( <header> <h2>{props.body}</h2> </header> ) // function App() { return <div> <Header body="나는 1번 헤더입니다."></Header> <Header body="나는 2번 헤더입니다."></Header> <Header body="나는 3번 헤더입니다."></Header> <Header body="나는 4번 헤더입니다."></Header> </div> }
<Header body="나는 1번 헤더입니다."></Header>처럼 Header안에 body라는 속성을 주게되는데, 이렇게 준 속성의 값은 props에 인자로 전달되고 그 안에 key, value의 형태로 저장된다. 그래서 props.body처럼 key 값을 이용해서 그 값을 가져올 수 있다.
여기서 header의 매개변수인 props는 임의적인 이름이지만, 대부분의 모든 사람들이 props를 사용하므로 굳이 다른 이름을 쓸 일은 없다고 한다.
const Nav = props => { const lis = props.topics.map(topic => { return ( <li key={topic.id}> <a id={topic.id} href={`/read/${topic.id}`}>{topic.title}</a> </li> ); }); return ( <nav> <ol>{lis}</ol> </nav> ); }; // function App() { const topics = [ { id: 1, title: "html", body: "html is..." }, { id: 2, title: "css", body: "css is..." }, { id: 3, title: "js", body: "js is..." }, ]; // return ( <div> <Nav topics={topics}></Nav> </div> }
위와 같이 map이나 for문을 이용해서 동적으로 ol 태그 안에 들어갈 li 태그를 생성할 수 있다. 위와 같은 경우에는 App() 안에 있는 topics 배열의 요소가 3개가 아닌 1억개라고 해도 우리가 코드를 변경할 필요 없이 동적으로 정보를 처리해 줄 수 있도록 만들어진 것이다.
하나 기억해야할 것은 React에서는 이렇게 동적으로 li 같은 요소를 생성할 경우 li 마다 고유한 값을 가진 key 라는 속성이 있어야한다는 것이다.
위에서도 보면,
<li key={topic.id}> <a id={topic.id} href={`/read/${topic.id}`}>{topic.title}</a> </li>
이렇게 li 의 속성으로 key라는 값을 주는 걸 확인할 수 있는데, 이건 React가 컴포넌트를 관라하기 쉽도록 도와주는 장치라고 한다. React를 사용하기 위한 규칙인 것 같으니 잘 기억하도록 하자.
이벤트를 설명하기에 앞서서, 일반적으로 JS에서 이벤트를 등록하기 위한 방법을 살펴보자.
- html tag 내에서 등록하기
<button id="submit-btn" onclick="changeColor()"></button> // <script> const changeColor = () => { ... } </script>
- addEventListener를 활용하기
<button id="submit-btn" onclick="changeColor()"></button> // <script> const submitBtn = document.querySelector("#submit-btn"); // submitBtn.addEventListener("click", changeColor); // const changeColor = () => { ... } </script>
이벤트를 등록하는 방법은 크게 두 가지로 정의할 수 있을텐데 한 가지는 태그 내에서 인라인으로 직접 이벤트를 등록하는 것이고, 나머지 하나는 별도의 script 태그나 js파일에서 addEventListener같은 메서드를 통해 이벤트를 등록하는 것이다.
React에서 이벤트를 등록하는 방식은 언뜻 보면 기존에 사용하던 인라인으로 이벤트를 등록하는 방법과 유사해보이지만, React에서는 props를 통해 이벤트 발생 시 실행되는 함수를 유동적으로 변경할 수 있으므로 유연성과 확장성에서 차이가 있는 듯 하다.
그리고 React 안에서 사용되는 {} 안에는 값 뿐만 아니라 표현식이나 함수도 들어갈 수 있는 것 같다.
const Nav = (props) => ( <Nav> <h2 onClick={(e)=>{ e.preventDefault(); props.onChangeMode(); }>이건 네비입니다.</h2> </Nav> ) const Header = (props) => ( <header> <h2>이건 헤더입니다.</h2> </header> ) // function App() { return <div> <Nav onChangeMode={()=>{ alert("hello!"); }></Nav> <Header></Header> </div> }
위와 같이 <Nav> 의 props 에 함수를 인자로 전달했는데, 이 역시도 다른 변수랑 마찬가지로 key 값으로 접근할 수 있다.
이벤트는 완성된 컴포넌트인 <Header></Header> 나 <Nav></Nav> 에서 등록하는 게 아니라 컴포넌트를 정의하는 선언부에서 이벤트를 정의해준다.
const Nav = (props) => ... 와 같이 선언부에서는 이벤트 발생 시 실행될 함수를 직접 등록하지 않고 변수로 등록한다. 완성된 컴포넌트에는 props에 전달할 인자로 이벤트 발생 시 실행될 함수 등을 전달하는데, 이렇게 하면 같은 이벤트라도 실행될 함수를 달리할 수 있으므로 활용성이 높아진다.
사실 위에서도 이미 조금씩 헷갈리기 시작했는데, State로 오면서 처음에 많이 헤맸던 것 같다.
일단 State를 설명하기에 앞서서 State가 왜 생겨났는지, 어떤 용도로 사용되는지 알아보기 위해서 다른 코드를 하나 살펴보자.
const Button = (props) => ( <button onClick={props.onChangeMode()}></button> ) const Header = (props) => ( <header>{props.currentMode}</header> ) // function App() { let darkMode = "on"; let currentMode = null; // if(darkMode === "on") { currentMode ="현재 다크모드가 켜져있습니다"; } else if(darkMode === "off") { currentMode ="현재 다크모드가 켜져있지 않습니다"; } // return ( <div> <Header currentMode = {currentMode}></Header> <Button onChangeMode={() => { darkMode = "on"; }}></Button> <Button onChangeMode={() => { darkMode = "off"; }}></Button> </div> ) }
위 코드를 살펴보면 darkMode 의 값에 따라서 currentMode에 입력되는 텍스트의 내용이 달라지는 걸 볼 수 있다. 그리고 그 currentMode의 값이 <Header></Header>에 props로 전달되는데, 화면에서는 <header></header>안에 내용물로 표시된다.
그리고 우리가 기대하는 것은 button을 눌렀을 때, darkMode의 값이 바뀌면 그에 따라서 <Header></Header>에 전달되는 currentMode의 값이 바뀌고 그것이 화면에 반영되는 것이다.
즉, darkMode를 off로 만드는 버튼을 클릭하면 화면이 바뀌어서 화면에 "현재 다크모드가 켜져있지 않습니다"라는 화면이 표시되는 것을 기대하는 것이다.
하지만 이렇게 코드를 작성해도 화면은 바뀌지 않는다. 왜냐하면 App() 함수는 처음 페이지가 로드될 때 한 번 실행된 뒤에는 다시 실행되지 않기 때문이다. 즉, 다시 렌더링되지 않기 때문에 darkMode의 값이 동적으로 변경된다고 하더라도 화면에는 반영되지 않는 것이다.
이런 상황에서 우리는 우리 코드의 상태를 관리해주는 State라는 개념에 눈이 가게된다.
State의 주요한 기능은 우리가 useState()를 사용해서 어떤 상태를 선언하면 React가 해당 상태를 지켜보게 만드는 것이다. 관찰하던 도중에 상태에 변화가 생기게 되면 그 변화를 감지하고 화면을 리렌더링해준다. 이는 ejs가 동적으로 화면을 불러올 때 화면을 새로고침하는 것과 다르게 새로고침 없이 화면을 다시 렌더링해준다는 점에서 차이가 있다.
그렇다면 State를 우리가 활용하려면 어떻게 해야할까.
import {useState} from 'react';
State를 사용하려면 이렇게 먼저 useState 를 import 해줘야한다.
import "./App.css"; import {useState} from 'react'; // const Header = (props) => ( <header> <h1> <a href="/" onClick={(e) => { e.preventDefault(); props.onChangeMode(); }} {props.title} </a> </h1> </header> ); // const Nav = (props) => { const lis = props.topics.map((topic) => { return ( <li key={topic.id}> <a id={topic.id} href={`/read/${topic.id}`} onClick={e=>{ e.preventDefault(); props.onChangeMode(e.target.id); }}>{topic.title}</a> </li> ); }); return ( <nav> <ol>{lis}</ol> </nav> ); }; const Article = (props) => ( <article> <h2>{props.title}</h2> {props.body} </article> ); // function App() { const [mode, setMode] = useState('WELCOME'); const [id, setId] = useState(null); const topics = [ { id: 1, title: "html", body: "html is..." }, { id: 2, title: "css", body: "css is..." }, { id: 3, title: "js", body: "js is..." }, ]; let content = null; if(mode === 'WELCOME') { content = <Article title="Welcome" body="Hello, WEB"></Article>; } else if (mode === 'READ') { const filter = topics.filter(topic => +topic.id === +id); const {_, title, body} = filter[0]; content = <Article title={title} body={body}></Article> } return ( <div> <Header title="REACT" onChangeMode={() => { setMode('WELCOME'); }} </Header> <Nav topics={topics} onChangeMode={(_id) => { setMode('READ'); setId(_id); }} </Nav> {content} </div> ); } // export default App;
위는 내가 수업을 들으며 작성해본 코드의 전문인데, 먼저 App() 함수를 살펴보자.
function App() { const [mode, setMode] = useState('WELCOME'); const [id, setId] = useState(null); ... }
App() 함수를 보면 useState()라는 함수를 사용하고 있고 인자로 'WELCOME'이라는 값을 전달하고 있다.
useState() 라는 함수는 두 개의 원소가 담긴 배열을 반환하는데, 예를 들어 내가 useState("darkMode") 라는 함수를 실행해서 const darkMode 라는 변수에 반환값을 할당했다고 해보자.
const darkMode = useState("darkmode"); console.log(darkMode);

console.log()를 찍어보면 useState() 함수의 반환값으로 배열이 반환된 것을 볼 수 있다. 배열의 첫 번째 원소는 우리가 설정한 'darkmode'라는 문자열이고, 두 번째 원소는 함수가 들어있다.
여기서 두 번째 원소에 들어있는 함수는 우리가 useState()로 설정한 상태를 변경할 수 있는 함수이다.
그래서 일반적인 경우에는 이런식으로 설정한다.
const _mode = useState("darkmode"); const mode = _mode[0]; const setMode = _mode[1];
일반적인 경우에, 우리는 밖으로 드러내고 싶지 않거나 다른 변수와 헷갈리지 않으려는 용도로 _(underscore)를 붙여서 변수명을 표시하고는 한다.
이렇게 해서 _mode에는 우리가 useState()의 반환값을 담고, mode에는 우리가 설정한 현재 상태 값이 들어가며 setMode에는 mode의 값을 변경할 수 있는 함수가 할당된다.
const _mode = useState("darkmode"); const mode = _mode[0]; const setMode = _mode[1]; // setMode("lightmode"); console.log(mode); // -> lightmode 출력
그래서 이를 구조분해를 활용하여 대부분 이런 식으로 축약하여 적고는 한다.
const [mode, setMode] = useState('darkmode');
그럼 우리가 직접 mode라는 변수에 lightmode라는 값을 할당하는 것과, setMode()를 이용해 값을 바꾸는 것은 어떤 차이가 있는가.
// 일반적인 생각 let mode = "darkmode"; mode = "lightmode";
// useState() 활용 const [mode, setMode] = useState('darkmode'); setMode("lightmode");
아마도 가장 중대한 차이점은 setMode 등과 같이 State를 변경하는 메서드가 실행되면, 그걸 관찰하고 있던 React가 상태가 변경되었다는 것을 감지하고 화면을 다시 렌더링해준다는 것이 아닐까 싶다.
이제 처음에 작성했던 코드를 다시 보자.
const Button = (props) => ( <button onClick={props.onChangeMode()}></button> ) const Header = (props) => ( <header>{props.currentMode}</header> ) // function App() { let darkMode = "on"; let currentMode = null; // if(darkMode === "on") { currentMode ="현재 다크모드가 켜져있습니다"; } else if(darkMode === "off") { currentMode ="현재 다크모드가 켜져있지 않습니다"; } // return ( <div> <Header currentMode = {currentMode}></Header> <Button onChangeMode={() => { darkMode = "on"; }}></Button> <Button onChangeMode={() => { darkMode = "off"; }}></Button> </div> ) }
우리가 darkMode 라고 설정했던 부분을 useState()로 바꿔보면 아래와 같이 다시 적을 수 있다.
import {useState} from 'react'; // const Button = (props) => ( <button onClick={(e) => { e.preventDefault(); props.onChangeMode(); }} {props.buttonName} </button> ); const Header = (props) => <header>{props.currentMode}</header>; // function App() { const [mode, setMode] = useState("on"); let content = null; if (mode === "on") { content = <p>ON!</p> } else if(mode === "off") { content = <p>OFF!</p> } // return ( <div> <Button buttonName="ON" onChangeMode={() => { setMode("on") }} </Button> <Button buttonName="OFF" onChangeMode={() => { setMode("off") }} </Button> {content} </div> ); }
이렇게 하면 버튼을 누를 때마다 화면이 다시 렌더링되는 것을 볼 수 있다.

공부 도중에 setMode()를 사용하면 진짜로 mode가 바뀌는지 확인하기 위해서 console.log()를 찍어 확인하려고 했었다.

이런 식으로 on 버튼을 누를 때랑, off버튼을 누를 때 각각 현재 모드가 무엇인지 출력해주는 코드이다.
예상대로라면, on버튼을 누르면 setMode("on")이 실행되어서 모드가 바뀌고 on이 출력되어야 하는데 실제론 그렇지 않았다.

실제로는 mode가 바뀌기 이전 값을 출력해주고 있었다. 내가 어떤 버튼을 누르던간에 바뀌기 이전 값이 먼저 출력되었다. 아마도 내가 방금 누른 값은 다음 번에 내가 버튼을 누를 때 출력될 거라고 예상할 수 있다.
혹시나 setMode()가 promise처럼 비동기여서 console.log()가 캐치하지 못하는 것은 아닐까?

await 아래에 점선이 뜨는 걸 보니 비동기 함수는 아닌 것 같다.

setTimeout() 함수를 사용해서 출력시간을 지연시켜봐도 결과는 같다.
그렇다면 현재까지의 결론은 setMode() 함수는 그 뒤의 함수들이 모두 실행되기 전에는 실행되지 않는다고 봐야 할 것 같다. 그게 아니면 함수는 실행되었지만 결괏값이 아직 mode에 반영되지 않았다거나.
그럼 그걸 확인해보기 위해서 console.log()를 버튼 클릭이 아니라 App() 함수 실행시에 실행되도록 하나 추가해보자.

25번 라인에 App() 함수가 실행되면 기본적으로 console.log()가 실행되도록 추가했고, 예상으로는 setMode() 함수가 실행될때마다 화면이 다시 렌더링되기 때문에 그 때마다 25번 라인에 있는 console.log()가 다시 실행될 터였다.
내가 궁금한 것은setTimeout()이 1초 뒤에 실행될 때, setMode()는 어떻게 동작하는가였다.

버튼을 눌렀을 때, setMode()는 즉시 실행되어서 App() 함수가 즉시 재실행되고 화면도 즉시 다시 렌더링되어 OFF!라는 문구가 뜨는 걸 볼 수 있다.
그런데 1초 뒤에 실행된 mode 에는 여전히 mode 가 on으로 설정되어 있었다. 의외의 결과였다. 왜냐면 나는 setMode()가 나머지 함수들을 모두 기다린 뒤에 실행되기 때문에 값이 반영되지 않았다고 생각했었는데, setMode() 는 즉시 실행되고 있었으니까.
또 하나 발견한 점.

setMode()를 실행했을 때, 이전 모드와 값이 같으면 화면이 다시 렌더링되지 않는다.
그렇다면 궁금한 점! 변수의 값은 달라졌는데, State가 그대로라면 화면은 다시 렌더링되지 않는 걸까?
setInterval()을 통해 1초마다 변수의 값을 증가시켜보자.
let count = 0;
setInterval(()=>{
count += 1;
console.log(count);
},1000);
const countNode = <p>{count}</p>
count라는 변수를 만들어서 화면에 띄우고, count의 값을 1초마다 1씩 늘려주는데, 그걸 console.log()로 출력해보자.

실험의 결과는 의외였는데,
난 처음에 State의 상태가 변하면 다시 렌더링이 이루어지면서 숫자가 증가하던 count의 값이 화면에 다시 반영될 줄 알았는데, 그게 아니라 그냥 App() 함수를 새로 하나 다시 실행하는 모양이었다. 그래서 setInterval()이 새로 다시 시작되면서 두 개의 setInterval()이 각각 동작하고 있는 모습이다.

그래서 setInterval()을 App() 함수의 밖으로 빼봤다. 전역으로 빠진 것이다.


이렇게 했더니 모드가 바뀔때마다 count가 새로 잘 렌더링되는 모습이다.
그렇다면 마지막으로 count가 바뀔 때마다 렌더링을 새로 하고 싶으면 어떻게 해야할까?
State를 사용하고 싶은데 App() 안에 count 변수를 선언하면 App()이 새로 실행되어버릴 때마다 우리가 저장하고자 했던 count의 값이 계속 초기화되는 일이 일어난다.
지금 모드가 변경될 때마다 App() 자체가 초기화되어 새롭게 실행된다면 App() 안이 아니라 바깥에 저장하면 되지 않을까?

첫 시도.
useState()를 App() 밖에서 즉 전역 스코프에서 사용하려고 하니, 여기서는 사용할 수 없다는 에러가 나온다. component 안에서만 사용해야한다고 한다. 이뤈...
function App() {
const [count, setCount] = useState("0");
setInterval(() => {
setCount(Number(count)+1);
console.log(count);
}, 1000);
...
}

이렇게 안으로 옮겨왔다.
이렇게 했더니 1초마다 함수가 다시 실행되어서 60초가 지나니까 setInterval()이 60개가 실행되는 참사가 일어났다. 그럼 어떻게 할까.
setTimeout()을 활용해볼까??
내가 의도한 것은 함수가 반복되는 것이었는데 예상과 다르게 반복의 반복이 추가로 일어나니, 반복을 없애면 내가 원하던대로 한 번만 반복되지 않을까?
function App() {
const [count, setCount] = useState("0");
setTimeout(() => {
setCount(Number(count)+1);
console.log(count);
}, 1000);
...
}

이렇게 하니 오류없이 잘 작동하는 모습이다.
setTimeout()은 일정시간 이후에 콜백함수를 한 번 실행하는 함수인데, setCount() 가 실행되면서 count의 숫자를 변경하면, 렌더링이 다시 실행되면서 setTimeout() 의 콜백함수가 실행될 때마다 다시 setTimeout()이 등록되어 유사 재귀함수의 형태가 된 것 같다.
App() 함수가 JS에서 우리가 init()이나 play()처럼 이름을 지어 사용하던 실행함수의 역할이라는 것을 이해가 되는 것 같다. 다만 중요한 것은 State로 선언한 상태들은 App() 함수가 다시 실행되며 렌더링되더라도 초기화되지 않는다는 사실이다.
해결되지 않은 문제점들을 풀려고 코드를 다시 적다보니 몇 개는 해결된 것 같아서 정리를 남기려고 한다.
State의 상태가 변경되면 App()함수가 다시 실행되며 화면이 리렌더링된다.그럼 App() 함수 안에서 실행되던 값들도 초기화된다. 정확히는 기존에 실행되던 App() 함수가 한 번 더 실행되는 것이다.State로 선언된 상태들은 App() 함수가 다시 실행되어도 초기화되지 않고 그 값을 유지한다.setInterval()이 아니라 setTimeout()을 활용한 유사 재귀의 형태로 함수를 구성해야 한다.State로 등록해야한다.아직 완전히 실행구조가 이해되는 것은 아니라서 화요일에 튜터님을 찾아가봐야겠다.