회원가입을 하면 원하는 문장을
저장할 수 있어요!
다음
AWS 이용 중이라면 최대 700만 원 지원받으세요
프론트엔드 개발자로서 업무를 진행할 때 고민되는 지점은 무엇이 있을까요? 물론 프론트엔드 성능, 크로스 브라우징, 사용자 경험처럼 많은 항목이 있지만, 개인적으로 개발자 경험 또한 중요한 항목이라 생각합니다. 사용자 경험과 비슷한 의미인 개발자 경험은 말 그대로 개발자가 개발을 하면서 경험하는 일들을 말합니다.
회원가입을 하면 원하는 문장을
저장할 수 있어요!
다음
회원가입을 하면
성장에 도움이 되는 콘텐츠를
스크랩할 수 있어요!
확인
프론트엔드 개발자로서 업무를 진행할 때 고민되는 지점은 무엇이 있을까요? 물론 프론트엔드 성능, 크로스 브라우징, 사용자 경험처럼 많은 항목이 있지만, 개인적으로 개발자 경험 또한 중요한 항목이라 생각합니다. 사용자 경험과 비슷한 의미인 개발자 경험은 말 그대로 개발자가 개발을 하면서 경험하는 일들을 말합니다.
흔히 ‘개발을 한다’고 하면 코드를 작성하는 일을 떠올립니다. 그러나 개발자로 일하다 보면 코드를 작성하는 시간보다 이미 작성된 코드를 읽는 시간이 훨씬 더 많다는 걸 깨닫게 됩니다. 이러한 상황은 혼자 개발할 때보다, 다른 사람들과 협업을 진행할 때 더욱 늘어나게 되는데요.
더 난해한 코드를 짤 수 있는 사람이 실력 있는 개발자로 통했던 1990년대와 달리, 요즘 실력 있는 개발자의 역량 중 하나는 ‘코드를 얼마나 이해하기 쉽게 만드는가’에 있습니다. 특히 여러 팀과 협업하거나, 규모가 큰 프로젝트를 진행할 땐 이러한 역량이 더욱 중요해집니다. 이번 글에서는 선언형 프로그래밍을 통해 리액트에서 이해하기 쉬운 코드를 작성하는 방법에 대해 살펴보겠습니다.
선언형 프로그래밍 또는 명령형 프로그래밍에 대해 들어본 적이 있나요? 리액트 코드 작성을 다루기에 앞서, 이 두 개념에 대해 숙지하고 있어야 합니다.
선언은 사전적 의미로는 ‘널리 펴서 말함, 또는 그런 내용’입니다. 그렇다면 선언형 프로그래밍은 무엇일까요? 선언형 프로그래밍이란 원하는 결과를 묘사하는 방식으로 코드를 작성하는 프로그래밍 패러다임입니다. 간단한 코드 예제와 함께 살펴보겠습니다. 자바스크립트를 사용해 배열에서 짝수만 필터링하는 예제입니다.
이 코드는 evenNumbers라는 새로운 배열을 만들어, 숫자 배열인 numbers 배열에 filters()라는 함수를 사용해 num % 2 === 0 조건에 맞는 요소들만 필터링해 저장합니다. 이 예시에서는 짝수를 어떻게 필터링해야 하는지가 아닌, 짝수를 필터링한 결과를 얻는 것에 초점이 맞춰져 있는데요. 이는 선언형 프로그래밍의 특징 중 하나로 ‘데이터를 어떻게 조작해야 하는지’가 아니라, ‘원하는 데이터는 무엇인지’에 집중하는 모습입니다.
이와 같이 선언형 프로그래밍 스타일로 코드를 작성하면 전체적인 가독성과 추상화 수준을 높여 개발자가 문제의 본질에 집중할 수 있도록 도와줍니다. 또한 이러한 작업을 통해 재사용성이 높고 병렬 처리가 유리한 특징을 가지게 됩니다.
다음은 명령형 프로그래밍입니다. 명령형 프로그래밍이란 선언형 프로그래밍과 대비되는 개념으로 ‘코드가 어떻게 동작해야 하는지’를 작성합니다. 예를 들어, 저녁 재료 쇼핑을 명령형 접근 방식으로 바꾸면 ‘각 재료를 장바구니에 추가한다’처럼 표현할 수 있는데요. 이번엔 코드 예시를 명령형으로 작성해 비교해 보겠습니다.
이전의 선언형 프로그래밍 예제와 달리 ‘numbers 배열의 길이만큼 반복’, ‘만약 num이 2로 나누어떨어진다면 evenNumbers 배열에 num 변수를 push’ 등 문제를 해결하는 과정을 작성한 모습이 보입니다. 이처럼 명령형 프로그래밍에서는 상태와 제어 흐름을 명시적으로 관리하는 방식으로 코드를 작성합니다. 따라서 코드의 가독성이 저하되거나, 재사용성이 낮아질 수 있어 코드 관리에 많은 노력이 필요합니다.
이제 선언형 프로그래밍과 명령형 프로그래밍의 차이점에 대해 살펴보았으니, 오늘의 주제인 리액트로 넘어가겠습니다. 우선 리액트의 공식 홈페이지에 접속하면 다음과 같은 내용이 적혀있습니다.
특히 선언형에 관한 내용 중 “애플리케이션의 각 상태에 대한 간단한 뷰만 설계하세요.”라는 문구가 눈에 띄는데요. 공식 홈페이지 소개와 같이 리액트는 선언형 UI를 지원하는 라이브러리로, 어떻게 활용할 수 있는지 알아보겠습니다.
먼저 간단한 로그인 폼을 보여주는 컴포넌트를 만들어볼 텐데요. 이번 글에서는 타입 스크립트를 사용해 함수 컴포넌트 방식으로 예제를 작성해 보겠습니다.
본 예제 코드는 선언형 UI로 작성되었습니다. 왜 이 코드를 보고 선언형 UI로 작성되었다고 하는 걸까요? 현재 본인이 원하는 LoginForm의 모습(결괏값)만 return 하고 있으며, 어떻게 UI를 화면에 보이게 할 건지는 작성되어 있지 않습니다. 즉, UI를 다루는 부분은 리액트에게 위임하고 개발자는 결과에만 초점을 맞춰 개발할 수 있는 것이죠. 이는 상태를 사용하는 조금 더 복잡한 예시에서도 다르지 않습니다.
이번엔 자체적인 카운트 상태 값을 가지고, 버튼을 클릭해 상태를 조작하는 카운터 컴포넌트입니다. 앞선 예제 코드와 달리 useState Hook과 onClick 등의 비교적 복잡한 구조를 보이고 있지만, 결국 해당 컴포넌트가 보여주고자 하는 UI만을 return 한다는 점은 변하지 않았습니다. 따라서 리액트는 선언형 UI 라이브러리라고 할 수 있습니다.
그렇다면 왜 선언형으로 작성하는 방법까지 알아야 할까요? 그 이유는 라이브러리가 선언형 UI를 제공한다고 해서 코드를 선언형으로 작성하는 것은 아니기 때문입니다. 실제로 개발을 진행하다 보면, 선언형 UI 라이브러리의 장점을 잃는 코드도 작성하게 되는데요. 따라서 자주 보는 예시와 이를 개선하는 방법을 살펴보겠습니다.
자바스크립트는 기본적으로 선언형 코드를 작성할 수 있는 메서드를 제공합니다. 대표적으로 map(), filter(), some(), every() 등이 있고, 어떻게 코드에 적용할 수 있는지 소개합니다.
예를 들어, 지원자 이름이 문자열 배열로 주어지고 이름을 입력해 지원자 목록에 있는지 확인하는 기능을 만들어보겠습니다.
위 예제 코드를 보면 appliers의 길이만큼 for문으로 반복하며, 만약 해당 배열 값에 searchName과 같은 값이 있다면 isApplied를 true로 바꿔주고 있습니다. 이는 전형적인 명령형 프로그래밍 방식으로 동작하는 데에는 문제가 없지만, 코드에 따라 가독성이 낮아지는 경우가 있습니다. 이 코드를 some() 메서드를 사용해 선언형 코드로 리팩토링해보겠습니다. 콜백 함수에서 배열의 요소 중 하나라도 true를 리턴하면 some() 메서드는 true를 리턴합니다.
어떤가요? 기존의 코드는 isApplied에 false를 할당한 후, for문을 통해 그 값을 변경하는 로직을 사용했으나 개선한 코드는 바로 할당하는 모습입니다. 이러한 개선을 통해 코드 길이가 줄어들었으며, 코드의 동작도 더 명확하게 보입니다. 이번에는 다른 예제를 보겠습니다.
본 예제는 사용자가 제출한 약관 배열이 모두 동의되었는지 확인하는 코드입니다. 이전 예제와 마찬가지로 for문을 통해 약관 배열의 길이만큼 반복하며, 동의하지 않은 약관이 있을 시 isAllTermsAgreed를 false로 변경합니다. 이번에는 some()이 아닌 every() 메서드를 사용해 선언형으로 작성해 보겠습니다. some() 메서드와 비슷하지만 콜백 함수가 배열의 모든 요소에서 true를 리턴해야, every()도 true를 리턴합니다.
결과는 9줄이던 코드가 3줄로 변경되었습니다. 물론 코드 길이가 짧다고 무조건 좋은 것은 아니지만, 이 코드 같은 경우 동작을 더 이해하기 쉬워짐은 물론 코드의 길이까지 줄었습니다.
또한 이 같은 메서드는 중첩으로 사용이 가능한데요. 만약 필수 약관에서 모든 약관에 동의했는지 확인할 땐 filter()와 every()를 함께 사용할 수 있습니다.
filter() 메서드를 사용하면 콜백 함수에서 true를 리턴하는 요소들로 새로운 배열을 생성할 수 있습니다. 이를 이용해서 콜백 함수가 필수 약관일 때만 true 리턴하도록 해, 필수 약관으로 구성된 새로운 배열을 생성합니다. 그리고 every() 메서드를 사용해 방금 생성한 필수 약관 배열의 약관들이 전부 동의하고 있는지를 확인합니다. 이 과정을 통해 ‘필수 약관만 추출’, ‘모든 필수 약관에 동의했는지 확인’ 두 가지를 진행할 수 있습니다.
이처럼 무심코 쓰는 코드들이지만, 기본 제공하는 메서드만으로도 선언형 코드를 작성할 수 있는데요. 지금 소개한 것 이외에도 다양한 메서드가 있으니 찾아보고 적용해 봐도 좋습니다.
실제 리액트로 프론트엔드 개발을 하다 보면, API를 사용해 서버의 데이터 값을 불러와 보여주는 경우가 많습니다. 예를 들어, 서버에서 할 일 목록을 받아와서 이를 보여주는 컴포넌트가 있다고 가정하겠습니다. 단, 데이터가 로딩중일 때는 로딩 인디케이터가 보여야 하며, 오류가 발생한 경우에는 fallback 컴포넌트가 보여야 합니다. 간단하게 코드를 구현해 보면 아래와 같습니다.
Todos라는 컴포넌트는 로딩 여부(loading)과 에러 여부(error) 상태를 가지고 있으며, useEffect에서 API로 요청하여 상태를 저장하고 있습니다. 그다음 아래쪽을 보면 에러가 발생했을 경우와 로딩 중일 경우, 그리고 둘 다 아닐 경우에 할 일(todo)들을 렌더링 해주는 모습이 보입니다.
이 부분을 리액트 공식 문서에서는 조건부 렌더링(Conditional Rendering)이라고 칭합니다. 컴포넌트가 특정 조건에 따라 다른 렌더링을 진행하는 것인데요. 이 예시의 경우 로딩 여부, 에러 여부에 따라 렌더링 결과물이 다른 것을 확인할 수 있습니다. 이 코드는 언뜻 보기에는 간단해 보입니다. 그러나 if문으로 이루어진 명령형 코드이기 때문에, 실제 프로젝트에서 더 많은 비즈니스 로직 핸들링이 들어간다면 컴포넌트가 비대해지고, 결과물을 한눈에 예측하기가 어려워집니다.
그럼 이러한 코드는 어떻게 선언형으로 개선할 수 있을까요? 리액트(18v 기준)에서는 Error Boundary와 Suspense를 사용한 제어의 역전(IoC)을 통해 개선할 수 있습니다. 쉽게 말해 로딩 또는 에러 상태에 따른 렌더링 제어권을 부모 컴포넌트에게 전가하고, 현재 컴포넌트는 보여주고 싶은 것에만 집중하는 방법입니다. 다음 예시 코드를 보겠습니다.
Todos의 return문이 하나로 통일된 모습이 보입니다. Error Boundary와 Suspense를 통해 Todos가 가지고 있던 조건부 렌더링 부분의 제어권을 부모 컴포넌트인 App으로 넘겨 처리하고 있는 모습인데요. 이렇게 작성하면 컴포넌트는 본인의 역할에 따른 관심사만 집중할 수 있기 때문에 이해하기 쉬운 선언형 컴포넌트를 만들 수 있습니다.
이 같은 패턴을 리액트에서는 Concurrent UI 패턴이라고 칭합니다. 이 방법을 통해 복잡한 비동기 컴포넌트에서도 결과물에 집중할 수 있는 코드를 만들 수 있습니다. (이번 글은 ErrorBoundary, Suspense의 동작 원리 주제가 아니어서 자세한 설명은 생략했습니다.)
지금까지 이해하기 쉬운 코드를 위한 선언형 프로그래밍에 대해 살펴봤습니다. 그렇다면 무조건 선언형으로 코드를 작성하는 게 더 좋은 방법일까요? 그렇지는 않습니다. 선언형 프로그래밍은 원하는 결과를 선언하고, 이를 얻기 위한 계산 과정을 추상화하는 작업이 포함됩니다. 이를 통해 코드의 가독성과 유지 보수를 향상시킬 수 있지만, 문제 해결을 위한 알고리즘의 세부 사항을 직접 제어할 수 없기 때문에 성능 저하나 예기치 못한 동작을 불러일으킬 수도 있습니다.
반면 명령형 프로그래밍의 경우 코드의 복잡성이 증가해 유지 보수가 어렵지만, 프로그램의 세부적인 제어 흐름을 결정할 수 있어 보다 정확한 결과를 얻을 수 있습니다.
따라서 선언형 혹은 명령형 프로그래밍 어느 하나만이 정답은 아닙니다. 현재 프로젝트 구조와 해결하고자 하는 문제의 성격, 요구사항 등을 고려해, 어떤 방식이 더 적절할지 팀 내부에서 충분한 논의가 필요합니다.
요즘IT의 모든 콘텐츠는 저작권법의 보호를 받는 바, 무단 전재와 복사, 배포 등을 금합니다.