Frontend 21

React 컴포넌트: 단순한 정리인가, UI의 뼈대인가?

파일을 나누고 props를 넘기는 일이 단순 정리인지, 데이터 흐름 자체를 만드는 일인지 헷갈린다.

근거 · 교안 p121-p134

React 컴포넌트: 단순한 정리인가, UI의 뼈대인가? 대표 이미지

1장: UI 조각을 함수처럼? 컴포넌트의 진짜 역할

솔라는 막 정리를 끝낸 책상을 만족스럽게 바라봤다. 이리저리 흩어져 있던 노트와 케이블, 필기구가 제자리를 찾자 마음까지 차분해지는 기분이었다. 모니터 한구석에 띄워 둔 메모 앱에도 방금 배운 문장이 깔끔하게 정리되어 있었다.

React 컴포넌트: UI 조각을 함수처럼 분리하여 재사용 가능한 부품으로 만든다.

솔라는 고개를 끄덕였다. “결국 이것도 정리 정돈 같은 거네. 큰 파일을 기능별로 잘게 나눠서 관리하기 쉽게 만드는 것.” 코드가 길고 복잡해지면 관련된 부분끼리 묶어 다른 파일로 옮기는 것. 솔라에게 ‘컴포넌트 분리’란 딱 그 정도의 의미였다. 잘 정리된 서랍장처럼, 필요할 때 찾기 쉽고 보기에도 좋은 상태로 만드는 코드 정리 기술.

그때, 솔라의 혼잣말을 들은 루나가 방 한쪽에 세워진 화이트보드 앞으로 다가갔다. 루나는 말없이 보드마커를 들어 커다란 사각형 하나를 그렸다. 그리고 그 안을 빼곡하게 채우기 시작했다. 맨 위에는 ‘우리들의 블로그’라는 헤더, 그 아래로는 게시물 목록이 그려졌다. 각 게시물은 제목, 작성자, ‘좋아요’ 버튼과 댓글 개수까지, 모든 요소가 뒤섞인 채 하나의 거대한 덩어리처럼 보였다.

“솔라, 이게 우리가 만들 게시판 페이지의 전체 모습이라고 해보자. 지금은 모든 게 App.js라는 파일 하나에 다 들어있어.”

루나가 그린 그림은 솔라가 방금 정리한 깔끔한 책상과는 정반대의 모습이었다. 보기만 해도 머리가 복잡해지는 거대한 스파게티 코드 뭉치를 눈앞에 시각화한 것 같았다.

“이걸 컴포넌트로 나눠보자. 솔라 네가 생각하는 ‘정리’의 방식으로.”

솔라는 자신 있게 나섰다. “음, 간단해. 일단 위쪽 헤더 부분은 따로 떼서 Header 컴포넌트로 만들고. 게시물 목록 전체는 양이 많으니까 PostList 컴포넌트로 만들면 되겠지. 파일이 너무 커지지 않게 적당히 나누는 게 중요하니까.”

솔라의 대답에 루나는 고개를 끄덕이는 대신, 빨간색 마커를 들었다. 그리고는 각 게시물에 그려진 ‘좋아요’ 버튼을 하나하나 짚었다.

“만약 기획자가 ‘좋아요’ 버튼을 하트 모양 아이콘으로 바꾸고, 클릭했을 때 색깔이 바뀌는 애니메이션을 넣어달라고 하면 어떡할래? 어디를 수정해야 하지?”

“그거야 PostList 컴포넌트 안에 있겠지. map 함수로 반복되는 부분에서… 어, 잠깐만.”

솔라는 말을 멈췄다. 자신의 제안대로라면 PostList는 단순히 게시물 목록의 ‘코드 덩어리’를 담고 있을 뿐이다. 그 안에는 게시물 하나하나의 구조, 제목, 작성자, 그리고 ‘좋아요’ 버튼의 모든 코드가 그대로 반복되어 들어있을 터였다. 버튼 하나를 바꾸려면 그 거대한 파일 안에서 반복되는 모든 버튼 코드를 일일이 찾아 수정해야만 했다. 하나라도 빼먹으면 일부 버튼만 구식 디자인으로 남게 될 것이다.

“좀… 번거롭겠다. 실수하기도 쉽고.”

“바로 그거야.”

루나는 미련 없이 화이트보드를 지우고 다시 커다란 사각형을 그렸다.

“이번엔 ‘파일 정리’가 아니라 ‘책임 분할’의 관점에서 다시 생각해보자. 이 화면의 각 부분은 어떤 ‘일’을 책임지고 있지?”

‘책임’이라는 단어에 솔라는 잠시 생각에 잠겼다. 루나는 게시물 하나를 네모로 감쌌다.

“이 네모의 책임은 뭘까?”

“어… 게시물 한 개를 온전히 보여주는 것. 제목, 작성자, 내용, 버튼까지 전부 포함해서.”

“좋아. 그럼 그 안에 있는 ‘좋아요’ 버튼. 이 작은 부분의 책임은?”

“자기가 받은 ‘좋아요’ 개수를 보여주고, 사용자가 클릭하는 이벤트를 처리하는 일.”

순간 솔라의 머릿속에서 무언가 ‘탁’ 하고 연결되는 느낌이 들었다. 아, 그래서 ‘함수’라는 비유를 썼구나. 함수는 저마다 정해진 역할이 있다. 입력을 받아 정해진 일을 처리하고 결과를 반환한다. 다른 함수가 뭘 하는지 시시콜콜 신경 쓰지 않는다. 그냥 자기 할 일만 제대로 하면 된다.

“알겠다! ‘UI 조각을 함수처럼 분리한다’는 말은 그냥 코드를 다른 파일에 옮겨 담는 게 아니었어. 각자 자기 일에만 집중하는 독립적인 부품으로 만드는 거구나. ‘게시물 하나 보여주기’라는 책임, ‘헤더 영역 표시하기’라는 책임, ‘좋아요 기능 수행’이라는 책임. 이렇게 UI의 역할을 기준으로 나누는 거였어!”

솔라의 눈이 빛났다. 컴포넌트 분리는 단순히 코드를 청소하는 행위가 아니었다. 복잡하게 얽힌 UI에 명확한 질서와 구조를 부여하는 설계의 영역이었다. 각 부품이 자신의 책임만 잘 수행하면, 나중에 ‘좋아요’ 버튼의 디자인이 바뀌든 기능이 추가되든, 딱 그 부품만 교체하거나 수리하면 그만이다. 전체 시스템을 뒤엎을 필요가 없는 것이다.

루나는 솔라의 깨달음을 확인하듯, 화이트보드에 새로운 구조를 그렸다. 최상위 App 컴포넌트 아래에 HeaderPostList가 있고, PostList 안에는 여러 개의 PostItem 컴포넌트가 자리를 잡았다. PostItem 안에는 다시 LikeButton이 그려졌다. 거대한 덩어리가 아닌, 명확한 책임의 계층 구조였다.

그림을 보던 솔라는 새로운 궁금증이 떠올랐다.

“언니, 책임별로 나눈 건 이제 완벽히 이해했어. 그런데 한 가지 이상한 점이 있어. PostList는 어떻게 여러 개의 PostItem한테 각각 다른 내용을 보여주라고 알려주는 거지? ‘넌 1번 게시물을 맡아’, ‘넌 2번 게시물이야’ 하고… 뭔가 데이터를 건네주는 방법이 있어야 할 것 같은데.”

책임의 분리는 명확해졌지만, 그 분리된 책임자들이 어떻게 서로 소통하고 협력하는지에 대한 문제는 이제 막 모습을 드러내고 있었다.

2장: 값 전달 그 이상: props가 만드는 데이터 흐름

화이트보드 위, 루나가 그린 컴포넌트 계층도에는 여전히 솔라와 루나의 시선이 머물러 있었다. 책임에 따라 명확하게 나뉜 구조. 솔라의 마지막 질문이 남긴 정적을 깬 것은 루나의 마커 펜이었다.

루나는 최상위 App 컴포넌트 옆에 작은 글씨로 데이터 예시를 적었다. posts = [{ id: 1, title: 'React 공부 시작!', ... }, { id: 2, ... }]. 그리고 App에서 PostList로, PostList에서 각각의 PostItem으로 향하는 화살표를 그렸다. 마지막으로, PostListPostItem을 가리키는 화살표 옆에 props라는 다섯 글자를 썼다. 추상적인 ‘소통’의 문제가 구체적인 ‘전달’의 경로로 바뀌는 순간이었다.

“아! props!”

솔라가 무릎을 탁 쳤다. 너무나 익숙한 개념이었다.

“함수에 인자 넘겨주는 거랑 똑같네. App이 가진 posts 데이터를 PostList에 통째로 넘겨주고, PostList는 그 배열을 돌면서 PostItem이라는 함수를 하나씩 호출하는 거야. ‘자, 여기 1번 게시물 데이터. 이걸로 화면에 그려줘’ 하면서.”

솔라에게 props는 그저 컴포넌트라는 함수를 호출할 때 필요한 값을 건네주는 편리한 매개변수(parameter)일 뿐이었다. 값 전달, 그 이상도 이하도 아니었다.

루나는 솔라의 비유를 부정하지 않고, 조용히 PostItem 컴포넌트 그림 하나를 손가락으로 가리켰다.

“함수 인자라… 그럼 이 PostItem이 자기가 받은 게시물 제목, ‘React 공부 시작!’이 마음에 안 든다고 해보자. 그래서 사용자 몰래 ‘React는 역시 어려워!’라고 제목을 직접 바꿔버리면 어떻게 될까? 함수 안에서 인자로 받은 변수 값을 바꾸는 것처럼 말이야.”

솔라는 잠시 어리둥절한 표정을 지었다.

“음… 그럼 화면에는 ‘React는 역시 어려워!’라고 보이겠지? 그게 왜…? 아.”

솔라의 말이 끊겼다. 그녀의 시선은 PostItem에서 출발해, 화살표를 거슬러 올라가 최상단의 App 컴포넌트 옆에 적힌 원본 데이터에 닿았다. 그곳의 제목은 여전히 ‘React 공부 시작!’이었다.

순간 머릿속에서 혼란스러운 장면이 그려졌다. 화면 한쪽에서는 PostItem이 제멋대로 바꾼 ‘React는 역시 어려워!’라는 제목을 보여준다. 하지만 만약 페이지 다른 곳에 있는 ‘최신 글 알림’ 컴포넌트가 App으로부터 직접 데이터를 받아 제목을 보여준다면? 그곳에는 원본 데이터인 ‘React 공부 시작!’이 표시될 것이다. 하나의 데이터가 UI의 위치에 따라 다르게 보이는 끔찍한 불일치.

“큰일 나네… PostItem이 마음대로 데이터를 고쳐버리면, 진짜 데이터의 주인인 App은 그 사실을 전혀 모르잖아. 데이터가 어디서 어떻게 바뀐 건지 추적하는 게 불가능해지겠어. 앱 전체가 뒤죽박죽 엉망이 될 거야.”

솔라는 스스로가 만든 혼란의 소용돌이를 들여다본 기분이었다. 버그를 잡기 위해 수십 개의 컴포넌트 파일을 열어보며 어디서 값이 잘못되었는지 밤새 추적하는 개발자의 모습이 눈앞에 선했다.

“그래서 React가 막아 놓은 거구나. props는 수정할 수 없다고.”

깨달음은 한순간에 찾아왔다. props의 읽기 전용(read-only) 원칙은 단순한 제약이 아니었다. 그것은 복잡한 UI의 상태를 예측 가능하게 유지하기 위한 핵심적인 규칙, 일종의 교통 신호등이었다.

“데이터는 항상 위에서 아래로만 흘러가야 해. 마치 폭포수처럼. 부모가 자식에게 값을 내려주지만, 자식은 거슬러 올라가 물줄기를 바꿀 수 없어. 그래야 우리는 데이터의 출처가 어디인지 명확히 알고, 앱의 상태를 믿을 수 있게 되는 거야. 이건 단순한 ‘값 전달’이 아니라, 예측 가능한 단방향 데이터 흐름을 만드는 거대한 규칙이었어.”

솔라가 말했다. ‘값 전달’이라는 얕은 이해가 ‘흐름 구축’이라는 깊은 통찰로 바뀌는 순간이었다. props는 컴포넌트들을 연결하는 단순한 케이블이 아니었다. 데이터가 흘러가는 방향과 규칙을 정의하는 UI의 혈관이었다.

루나는 솔라의 말에 고개를 끄덕이며, 화이트보드의 화살표들을 모두 한쪽 방향으로만 흐르도록 굵게 덧칠했다. 이제 컴포넌트 계층도는 명확한 데이터의 흐름을 보여주는 지도처럼 보였다.

지도를 보던 솔라의 시선이 여러 개의 PostItem이 나란히 늘어선 PostList 영역에 멈췄다. 새로운 질문이 싹텄다.

“데이터 흐름은 이제 알겠어, 언니. 그런데 PostList가 가진 게시물 데이터 배열로 이 PostItem들을 여러 개 만들 때 말이야. 얘네들은 전부 똑같이 생긴 컴포넌트잖아. React는 1번 게시물이랑 2번 게시물을 어떻게 구분하는 거지? 만약 목록 순서가 바뀌거나 중간에 하나가 삭제되면… 헷갈리지 않을까?“

3장: 목록 속 숨겨진 식별자: key prop의 중요성

데이터가 흐르는 길이 명확해진 화이트보드 지도. 솔라의 마지막 질문이 남긴 여운이 가라앉자, 루나는 말없이 책상 서랍에서 색색의 포스트잇 한 묶음을 꺼내 들었다. 그리고는 화이트보드의 PostList 컴포넌트 아래, 나란히 늘어선 PostItem 그림들을 가리켰다.

루나는 노란색 포스트잇 세 장을 떼어, 그림 위에 하나씩 덮어 붙였다. 각 포스트잇은 마치 방금 막 렌더링된 컴포넌트 인스턴스처럼 보였다. 하지만 겉보기에는 내용물도, 특별한 이름도 없는 그저 똑같이 생긴 세 개의 노란 사각형일 뿐이었다. 솔라가 제기했던 문제, 즉 React가 이 ‘똑같이 생긴 컴포넌트들’을 어떻게 구분할 것인가에 대한 질문을 그대로 시각화한 것이다.

“React가 게시물 목록을 처음 화면에 그린다고 상상해보자.”

루나가 첫 번째 포스트잇을 툭툭 치며 말했다.

“여기 게시물 세 개에 해당하는 PostItem 컴포넌트 세 개가 있어. 솔라 네 말대로, React 입장에선 얘네들이 다 똑같이 생겼지. 그럼 그냥 순서대로 자리를 맡기면 되지 않을까? 첫 번째 데이터는 첫 번째 컴포넌트에, 두 번째 데이터는 두 번째 컴포넌트에.”

솔라는 고개를 끄덕였다. 그게 가장 상식적인 방법처럼 보였다. “응, 배열의 인덱스(index)처럼. 0번, 1번, 2번 자리에 각각 데이터를 넣어주는 거지. 간단하잖아.” 그녀에게 이 문제는 그저 순서 맞추기 게임 정도로 여겨졌다. React가 이 정도를 못할 리 없다고 생각했다.

루나는 그 대답을 기다렸다는 듯, 빨간색 마커를 들었다.

“좋아, 그럼 이 상태에서 사용자가 2번 게시물을 삭제했다고 해보자. 이제 우리 데이터는 1번과 3번 게시물만 남았어. React는 어떻게 할까?”

루나는 두 번째 포스트잇을 손가락으로 튕겼다.

“간단하지. 그냥 1번이랑 2번만 남기고… 아, 아니지. 데이터가 바뀌었으니까, 두 번째 자리에 있던 2번 게시물 컴포넌트를 없애고, 세 번째 자리에 있던 3번 게시물 컴포넌트를 그 자리로 옮기면 되겠다.”

솔라의 머릿속에서는 자연스럽게 빈자리를 채우는 모습이 그려졌다. 하지만 루나는 고개를 저었다.

“그건 우리가 ‘내용’을 아니까 가능한 생각이야. React는 그저 ‘컴포넌트 목록의 길이가 3에서 2로 줄었다’는 사실만 알아. 그래서 가장 효율적인 방법을 택하지.”

루나는 말을 이으며 포스트잇 위에서 손가락을 움직였다.

“React는 기존 컴포넌트들을 재사용하려고 해. 첫 번째 자리? 그대로 있네. 첫 번째 데이터(1번 게시물)를 그냥 넣어주자. 두 번째 자리? 어, 얘도 그대로 있네. 그럼 이제 두 번째 데이터가 된 ‘3번 게시물’의 내용을 여기다 넣어주자. 그리고 세 번째 자리는? 이제 필요 없으니 없애버리자.”

루나는 세 번째 포스트잇을 떼어 구겨버렸다. 솔라는 눈앞에서 벌어진 상황을 잠시 이해하지 못했다. 분명 2번 게시물이 삭제되었는데, 엉뚱하게 3번 컴포넌트가 사라졌다. 그리고 원래 2번 게시물을 보여주던 컴포넌트는 이제 3번 게시물의 내용을 보여주고 있었다.

“잠깐, 뭔가 이상해. 만약 2번 게시물 컴포넌트 안에 사용자가 ‘내용 더 보기’를 클릭해서 펼쳐놓은 상태였다면 어떻게 되는 거야?”

“그 상태는 그대로 남아있겠지.”

루나의 대답에 솔라의 등골이 서늘해졌다. 상상 속 화면에서는 끔찍한 일이 벌어지고 있었다. 사용자는 분명 3번 게시물 제목을 보고 있는데, 그 내용은 엉뚱하게 ‘더 보기’가 펼쳐진 상태로 보이는 것이다. 데이터와 UI 상태가 완전히 어긋나 버리는 재앙.

“말도 안 돼… 그럼 목록 순서를 바꾸거나, 맨 앞에 새 게시물을 추가하면 전부 엉망진창이 되겠네. React가 그렇게 멍청할 리가 없는데.”

“멍청한 게 아니라, 우리가 신분증을 안 줬기 때문이야.”

루나는 파란색 마커를 들어 남은 두 개의 포스트잇 위에 작은 글씨를 써넣었다. 첫 번째 포스트잇에는 key: "post-1", 두 번째 포스트잇에는 key: "post-3" 이라고 적었다. 처음부터 2번 게시물 포스트잇에는 key: "post-2"가 적혀 있었다고 상상하라는 듯이.

“이게 바로 key라는 특수한 prop이야. 목록에 있는 각 항목에 붙여주는, 절대 중복되지 않는 고유한 이름표지.”

루나가 다시 물었다. “자, 이제 2번 게시물이 삭제됐어. React는 이름표가 붙은 포스트잇들을 보고 어떻게 할까?”

솔라는 이제야 모든 조각이 맞춰지는 것을 느꼈다.

“아! 이제 순서가 아니라 이름표를 보겠구나! React는 key: "post-1"이 여전히 목록에 있는 걸 확인하고, key: "post-2"가 사라진 걸 발견하겠지. 그리고 key: "post-3"도 그대로 있는 걸 볼 거야. 그럼 정확하게 key: "post-2" 이름표를 가진 컴포넌트만 찾아서 없애면 돼. 다른 애들은 건드릴 필요도 없이!”

key는 단순한 최적화 도구나 부가 기능이 아니었다. 그것은 동적인 목록 속에서 각 항목의 ‘정체성’을 보장하는 핵심적인 장치였다. 수많은 학생이 앉아있는 교실에서 자리가 바뀌어도 이름으로 학생을 정확히 찾아낼 수 있는 것처럼, key는 React가 UI의 변화 속에서 길을 잃지 않도록 붙잡아주는 유일한 단서였다.

솔라는 비로소 모든 것을 꿰뚫는 하나의 그림을 완성했다. 그녀는 화이트보드로 다가가, 루나가 그려놓은 컴포넌트 계층도를 가만히 들여다보았다. 거대한 App에서 출발해 PostList를 거쳐 여러 개의 PostItem으로 이어지는, 이제는 너무나 명확해진 구조였다.

솔라는 파란색 마커를 집어 들었다. 그리고 PostList가 여러 개의 PostItem을 만들어내는 부분에, 배운 모든 것을 담아 한 줄의 코드를 조심스럽게 그려 넣었다.

<PostItem key={post.id} post={post} />

책임의 분할, 단방향 데이터 흐름, 그리고 이제는 고유한 정체성까지. 단순한 ‘정리’라고 생각했던 컴포넌트 분리는, 사실 UI라는 살아있는 유기체의 뼈대를 세우고 혈관을 연결하며, 각 세포에 이름을 붙여주는 창조의 과정 그 자체였다.