이 코드는 예측 가능한가?
항상 코드를 작성할 때 중요시해야 하는 일은 코드를 잘 짜는 것일 겁니다.
문제는 이 “잘 짜여진 코드”라는 것을 정의하는 객관적인 기준이 없다는 점입니다.
하지만 통상적으로 잘 짜여진 코드란 “읽기 쉬운 코드” / “이후 유지보수에 유리한 코드” / “버그가 적은 코드” 등등일 것입니다.
좀 더 간단히 말하면 문제가 일어날 확률이 적은 코드입니다.
그렇다면 문제가 일어날 확률을 줄이려면 어떻게 해야 할까요?
결론부터 말하자면 예측 가능한 코드를 작성해야 합니다.
정상적인 개발자라면 일부러 문제가 있을 코드를 작성하는 경우는 거의 없을 겁니다.
그럼에도 문제가 일어날 가능성이 존재하는 이유는 비록 내가(또는 우리 팀이) 작성한 코드라도, 이 코드가 어떠한 동작을 할지 예측이 안 되기 때문입니다.
따라서 예측 가능한 코드를 작성하게 되면 문제가 일어날 확률이 낮아지고, 결과적으로 “잘 짜여진 코드”를 작성할 확률이 올라갑니다.
사실 위의 말은 이상적이고, 어떻게 보면 허황된 말일 수 있습니다.
앱/웹 전체 코드를 예측 가능한 코드로 작성한다는 것은 거의 불가능에 가깝기 때문입니다.
하지만 조금만 범위를 좁혀서 해당 앱/웹을 구성하는 구성요소 하나하나를 예측 가능하게 만들 수만 있다면, 그리고 그러한 구성요소가 모인 앱/웹이라면 현실적인 수준에서 예측 가능한 앱/웹 구현이 가능할 것입니다.
이 글에서는 결론적으로 앱/웹을 구성하는 구성요소를 예측 가능하게 하는 방법을 알아볼 것이지만, 지금은 조금 더 범위를 좁혀서 함수를 예시로 들어보겠습니다.
다들 알다시피 함수는 입력을 받고(또는 입력 없이) 함수 내부에서 어떠한 계산을 수행한 다음 결과를 내보내는(또는 결과 없이) 일종의 로직 블록입니다.
이 함수를 예측 가능하게 하려면, 또는 이 함수의 예측 가능성을 낮추는 요소를 하나씩 제거하면 예측 가능한 좋은 함수가 될 수 있을 것입니다.
그렇다면 무엇이 함수의 예측 가능성을 낮추는지 알면 이 접근이 가능해질 것입니다.
무엇이 함수의 예측 가능성을 낮추는가?
위에서 언급했듯이 함수는 “입력 → 계산 → 출력”을 수행합니다.
문제는 위 입력
, 계산
, 출력
외 요소가 포함되거나, 외부 요소에 영향을 주는 것이 사이드 이펙트입니다.
예를 들어 아래와 같은 코드를 보면,
const todos = [];
function addTodo(todo) {
todos.push(todo);
}
addTodo
함수는 todo
라는 입력을 받고 todos.push(todo);
라는 계산을 수행하며, 아무것도 반환하지 않습니다.
이 함수는 잘 작동하지만, 외부 요소인 todos
에 영향을 주기 때문에 사이드 이펙트를 일으킵니다.
또 다른 예시는,
function getSomePostByUserId(userId) {
return fetch(`https://api.someapi.com/posts?userid=${userId}`).then((res) =>
res.json(),
);
}
getSomePostByUserId
는 외부 요소에 영향을 주지 않는 것처럼 보이지만, 반대로 외부 요소(네트워크)에 의해 영향을 받습니다.
이 함수가 반환하는 값은 fetch
성공 여부에 따라 달라지며, 그 성공 여부는 함수의 제어 범위를 벗어나 있습니다.
두 예시를 종합하면, 사이드 이펙트란 외부에 영향을 주거나 / 외부로부터 영향을 받는 상황이라고 볼 수 있습니다.
또 다른 예측 가능성을 낮추는 부분은 일관되지 않은 출력입니다.
함수가 동일한 입력에 대해 동일한 출력을 반환하지 않는다면, 이는 사이드 이펙트가 발생할 가능성이 높다는 뜻입니다.
예컨대 getSomePostByUserId
에 userId = 1
을 고정한 뒤 함수를 100번 호출하면, 일부 호출 결과는 다른 호출 결과와 다를 가능성이 존재합니다.
다시 본론으로 돌아가면, 사이드 이펙트를 최소화해야 예측 가능한 코드를 작성할 수 있습니다.
(사이드 이펙트가 없는 것이 이상적이지만, 현실적으로는 불가능에 가깝습니다.)
이러한 함수를 순수 함수라고 부르며, 이 개념이 리액트의 출발점이 됩니다.
컴포넌트는 함수다
앞에서 내용을 정리해 보겠습니다.
- 좋은 웹/앱은 예측 가능한 코드에서 출발한다.
- 함수의 예측 가능성을 낮추는 것은 사이드 이펙트다.
여기에 하나만 더 추가하면 리액트 이야기를 시작할 수 있습니다.
리액트의 구성요소인 컴포넌트는 함수입니다.
컴포넌트가 무엇인지 말하기 전에, 리액트에 대해 간단히 설명해 보면,
리액트는 선언적 방식으로 UI를 구현하는 데 도움을 주는 라이브러리입니다. (참고: 리액트 공식문서 - The library for web and native user interfaces)
저는 이 문장을 처음 봤을 때 전혀 이해하지 못했습니다.
“선언적 방식”이 무엇인지, 왜 프레임워크가 아니라 라이브러리인지 등등 궁금했습니다.
위 문장을 제가 이해하기 쉬운 문장으로 바꿔 보면,
“리액트를 사용하는 개발자가 리액트가 제공하는 여러 툴로 원하는 UI(HTML + JS로 이루어진 뷰)를 선언하면, 리액트가 알아서 그려 준다.” 정도로 이해했습니다.
조금 더 구체적으로 살펴보면, 리액트는 기본적으로 JSX
라는 문법을 사용합니다.
어렵게 설명할 수도 있지만, 쉽게 말하면 “JS를 사용할 수 있는 HTML” 혹은 “JS 기능을 활용해 HTML을 작성할 수 있게 해 주는 문법” 정도로 이해할 수 있습니다.
HTML은 선언적인 방식으로 구조화된 마크업을 그릴 수 있게 해 줍니다.
여기서도 “선언적”이라는 말이 나오는데, 예시를 보면 바로 이해할 수 있습니다.
<nav>
<h1>Logo</h1>
<ul>
<li>Nav Item 1</li>
<li>Nav Item 2</li>
<li>Nav Item 3</li>
</ul>
</nav>
우리는 단순히 HTML 태그를 이용해 “이건 여기, 저건 저기”라고 선언만 했는데, 이 코드를 실행하면 실제로 그 구조가 그려집니다.
이것이 바로 선언적 방식입니다. 즉, 어떤 기대를 가지고 코드를 작성하면, 디테일한 지시를 하지 않아도 알아서 그려 줍니다.
그렇다면 JS는 어떨까요? JS는 자바스크립트 문법을 이용해 복잡한 문제를 비교적 간단히 해결할 수 있게 도와주는 툴 모음이라고 볼 수 있습니다.
다시 React 설명으로 돌아오면, “리액트는 선언적 방식으로 UI를 구현하는 데 도움을 주는 라이브러리”라는 문장 중 UI를 구현 부분은 JSX
로 해결됩니다.
우리가 JSX
를 작성하면, 리액트가 내부적으로 이를 처리해 UI를 그려 주는 것이죠.
이제 “선언적”이라는 키워드에 집중해 보면 이 문장이 완전히 이해됩니다.
JSX
가 좋은 점은 알겠지만, 근본적 문제가 남습니다.
이 JSX
를 어떻게 재사용할까? → 여기서 컴포넌트라는 개념이 등장합니다.
컴포넌트는 **“입력을 받아 UI 설명을 반환하는 함수”**입니다.
여기서 입력은 props
(속성)이고, 반환값은 JSX(= UI 설명)입니다.
만약 같은 props
를 넣으면 항상 같은 JSX가 나오도록 만든다면, 이 컴포넌트는 순수 함수가 됩니다.
export default function BookList({ books }) {
return (
<section>
<h2>Favorite Books</h2>
<ul>
{books.map((title) => (
<li key={title}>{title}</li>
))}
</ul>
</section>
);
}
- 입력 →
books
배열 - 계산 →
map
으로 리스트를 JSX로 변환 - 출력 →
<section> ... </section>
외부 상태를 건드리지 않고, 네트워크 요청도 없으며, 같은 books
가 들어오면 언제나 같은 UI를 그려 줍니다.
즉, 사이드 이펙트가 없는 순수 함수이고, 그래서 예측 가능합니다.
그래서 리액트는 예측 가능한 컴포넌트를 요구합니다.
컴포넌트는 결국 함수입니다. 함수가 예측 가능하려면 입력과 출력이 명확하고, 불필요한 부수 효과가 없어야 하며, 하나의 역할만 맡아야 한다는 세 가지 원칙을 기억해야 합니다.
첫째, 컴포넌트의 입력은 props
이고 출력은 JSX입니다. 같은 props
가 들어오면 언제나 같은 화면이 그려진다는 사실이 보장돼야 UI를 신뢰할 수 있습니다.
둘째, 컴포넌트 본문 안에서 네트워크 요청이나 콘솔 로그처럼 외부 세계에 영향을 주는 코드를 실행하면 그 순간부터 출력이 예측 불가능해집니다. React 팀은 이런 부수 효과를 이벤트 핸들러나 훅(useEffect
) 안으로 격리해 “순수 컴포넌트”를 지키라고 권장합니다.
셋째, 컴포넌트를 기능 단위로 잘게 나누면 테스트와 리팩터링이 간편해집니다. 하나의 책임만 가진 작은 함수는 테스트 대상이 명확하고, 수정해도 다른 영역에 미치는 파급력이 제한적입니다.
즉, 컴포넌트를 순수 함수처럼 다루면 우리가 함수에서 배운 예측 가능성을 UI 계층에도 그대로 적용할 수 있습니다.
정리하면 React는 “HTML의 선언성과 JavaScript 함수·모듈 시스템을 결합해 예측 가능한 UI를 만들도록 돕는 라이브러리”입니다.
- 개발자는 JSX로 화면을 선언하고,
- UI 조각을 컴포넌트라는 재사용 가능한 순수 함수로 감싸며,
- 동적인 상태 변화나 외부 요청 같은 사이드 이펙트는 훅(Hooks) 으로 격리합니다.
React 팀이 궁극적으로 추구하는 목표는 예측 가능한 컴포넌트로만 구성된 애플리케이션을 쉽게 만들 수 있게 해 주는 것입니다.
그렇게 완성된 앱은 읽기 쉽고, 유지보수가 수월하며, 버그가 발생할 여지가 적습니다.
결국 “좋은 웹”을 구현하기 위한 출발점은 예측 가능한 코드이고, React는 그 철학을 UI 세계에 가장 자연스럽게 녹여낸 도구라고 할 수 있습니다.