Загружаем каталог…
Загружаем каталог…
『오브젝트』 공부를 시작하고 객체의 자율성과 캡슐화, 책임과 협력에 대해 배우고 있습니다. 프로젝트 코드를 작성하고 팀원들과 치열하게 리뷰를 주고받다 보니, 책에서 읽은 내용을 자연스럽게 우리가 사용하는 컴포넌트와 훅에 대입해보게 됐어요. 그 과정에서 자주 떠오른 질문이 있습니다. 훅은 정말 하나의 일만 해야 할까요? 흔히 단일 책임 원칙(SRP)을 ‘하나의 모듈은 하나의 일만 해야 한다’는 뜻으로 이해하곤 하죠. 그 말대로라면 훅도 하나의 일만 해야 할 것 같지만, 실제 훅을 열어보면 상태를 읽는 코드도, 값을 계산하는 코드도, 다른 훅을 부르는 코드도 함께 들어 있습니다. 어디까지 함께 두고 어디서 나눠야 할지, 막상 판단하려면 쉽지 않죠. 이 글은 그 답을 찾아가는 과정의 일부입니다. 『오브젝트』가 말하는 자율적인 객체 를 훅에 대입해보고, 프로젝트 “knot”의 녹음 화면 PR에서 나눈 논의를 따라가며 훅의 책임에 대한 제 생각이 어떻게 달라졌는지 정리해보려 합니다. 객체는 스스로 일한다 『오브젝트』를 읽는 내내 가장 자주 마주친 단어는 자율성 이었습니다. 좋은 객체지향 설계에서는 모든 객체가 스스로 판단하고 행동해야 한다고 하죠. 그렇다면 스스로 일한다는 건 어떤 모습일까요? 자기 상태는 자기가 관리하고, 요청을 받으면 어떻게 처리할지도 자기가 정하는 것입니다. 그러려면 필요한 정보와 그 정보로 하는 행동이 같은 곳에 있어야 해요. 책의 영화 예매 예제로 비교해보면 차이가 바로 보입니다. // 바깥에서 요금을 꺼내 대신 계산한다 fee = movie.getFee().minus(discountAmount).times(audienceCount); // Movie에게 계산을 요청한다 fee = movie.calculateMovieFee(screening); 첫 번째 코드에서 Movie 는 요금을 담아두는 상자에 가깝습니다. 할인 규칙이 바뀌면 Movie 가 아니라, 요금을 꺼내 대신 계산하던 바깥 코드를 고쳐야 하죠. 두 번째 코드에서는 Movie 가 스스로 계산합니다. 요금이 몇 가지든, 어떻게 계산되든 바뀌는 곳은 Movie 내부뿐이에요. 변경의 주체가 바깥이 아니라 자기 자신일 때 , 그 객체를 자율적이라고 부를 수 있습니다. 그렇다고 자율적인 객체가 모든 일을 혼자 하는 건 아니에요. 두 번째 코드의 calculateMovieFee(screening) 처럼 스스로 할 수 없는 일은 다른 객체에게 메시지 를 보내 부탁합니다. 이렇게 자율적인 객체들이 메시지를 주고받으며 하나의 기능을 완성하는 것을 협력 이라고 부릅니다. 자율성을 지키는 방법, 캡슐화 객체를 자율적으로 만드는 방법은 내부 구현을 캡슐화 하는 것입니다. 바뀔 가능성이 높은 부분은 안에 숨기고, 비교적 안정적인 부분만 밖에 공개하는 거예요. 캡슐화라고 하면 ‘그냥 감추는 것 아닌가?’ 싶을 수 있어요. 하지만 캡슐화는 무엇을 하는지만 보여주고 어떻게 하는지는 내부에 두는, 추상화의 한 종류입니다. 호출부가 내부 구현을 모를수록 객체는 호출부를 신경 쓰지 않고 내부를 바꿀 수 있으니, 캡슐화는 사실상 자율성을 지키는 가장 기본적인 장치라고 볼 수 있죠. 여기서 오해하기 쉬운 부분이 있습니다. 캡슐화는 필드를 private 으로 막는 것만을 뜻하지 않아요. fee 를 private 으로 막아도 getFee() 로 그대로 꺼내준다면, 호출부는 여전히 ‘Movie 안에 요금이 있다’는 사실에 기대어 직접 계산합니다. 그래서 요금을 저장하는 방식이 바뀌는 순간, getFee() 로 요금을 꺼내 계산하던 호출부도 모두 고쳐야 하죠. 필드는 숨겼지만, 내부 구조는 그대로 드러내고 있었던 셈이에요. 캡슐화가 지키려는 건 필드가 아니라 내부가 바뀌어도 바깥은 그대로인 상태 입니다. 한마디로, 변경의 영향을 객체 안에 가두는 일이에요. 응집도와 결합도 캡슐화가 잘 되었는지는 응집도와 결합도, 두 가지 척도로 확인할 수 있습니다. 응집도 : 하나의 모듈 안에 서로 관련된 코드만 모여 있는 정도 → 저는 이걸 “같은 이유로 함께 바뀌는가?” 라는 질문으로 이해하고 있어요. 결합도 : 하나의 모듈이 다른 모듈의 내부를 알고 있는 정도 → 한 모듈을 고쳤을 뿐인데 그 모듈을 쓰던 코드까지 줄줄이 고쳐야 한다면, 결합도가 높은 거예요. 캡슐화를 지키면 바뀔 이유가 모듈 안으로 모여 응집도는 높아지고, 모듈을 쓰는 코드가 알아야 할 것이 줄어 결합도는 낮아집니다. 정리하면 자율성, 캡슐화, 응집도와 결합도는 모두 같은 곳을 가리키고 있어요. 변경을 한곳에 가두는 것. 그렇다면 훅도 자율적일 수 있을까요? 책을 읽다 보면 자꾸 ‘객체’ 자리에 컴포넌트와 훅을 넣어 읽게 됩니다. 컴포넌트와 훅도 상태와 로직을 함께 가지고, 서로 값을 주고받으며 협력하니까요. 그중에서도 훅은 일반 함수와 조금 다릅니다. 일반 함수 는 불렸을 때만 실행되고, 결과를 받은 컴포넌트가 다음 일을 정합니다. 훅 은 React 생애주기 안에서 상태를 기억하고, 이펙트로 스스로 일을 시작할 수도 있어요. 그래서 저는 로직을 훅으로 묶어 둔다는 걸, 그 훅을 하나의 자율적인 존재로 본다 는 뜻으로 이해했어요. 일반 함수로만 묶으면, 다음 일을 정하는 책임의 일부가 여전히 호출하는 컴포넌트에 남으니까요. 물론 반대도 성립합니다. 상태도 생애주기도 필요 없는 순수한 계산이라면 굳이 훅으로 감쌀 이유가 없죠. React 공식 문서 도 훅을 쓰지 않는 함수에는 use 를 붙이지 말라고 안내합니다. 그래서 컴포넌트의 로직을 훅으로 꺼낼지는 ‘몇 줄이나 되나’보다 ‘스스로 기억하거나 시작해야 하는 일인가’ 로 판단할 수 있어요. 그렇다면 훅으로 꺼내 컴포넌트는 그리기만 하게 두고, 아니라면 함수로 충분하죠. 이렇게 자율적인 객체를 훅으로 옮겨 보니, 자율적인 훅이 갖춰야 할 조건을 세 가지로 정리할 수 있었어요. 자기 일은 스스로 한다. 컴포넌트가 일일이 지시하지 않아도, 필요한 상태와 생애주기를 알아서 다룬다. 모르는 일은 맡긴다. 스스로 할 수 없는 일은 다른 훅이나 함수에 부탁하며 협력한다. 어떻게 하는지는 감춘다. 사용하는 쪽은 무엇을 해주는지만 알면 되고, 내부 구현을 몰라도 반환값만 보고 협력할 수 있다. 그런데 이 기준을 들고 실제 코드를 읽어보니, 새로운 고민이 생겼습니다. 녹음 바의 훅을 살펴봅니다 녹음 화면의 RecorderBar 는 경과 시간을 보여주고, 녹음을 일시 정지하거나 끝낼 수 있는 컴포넌트입니다. 필요한 값과 동작은 모두 useRecorderBar 에서 가져와요. 스타일과 일부 버튼을 생략하면 다음과 같습니다. function RecorderBar() { const { elapsedTime, handlePause, handleEnd } = useRecorderBar(); return ( <div> <span>{elapsedTime}</span> /* 경과 시간 */ <button onClick={handlePause}>일시 정지</button> <button onClick={handleEnd}>녹음 끝내기</button> </div> ); } 컴포넌트는 받은 값을 그리기만 합니다. 그렇다면 useRecorderBar 안에서는 무슨 일이 일어날까요? 설명에 필요한 부분만 남기면 이렇습니다. export const useRecorderBar = () => { // 1. 공통 녹음 기능과 화면 이동 기능을 가져온다 const { elapsedSeconds, startRecording, pauseRecording, endRecording } = useRecording(); const { navigateToHome } = useNavigateToHome(); // 2. 화면에 들어오면 녹음을 시작한다 useEffect(() => { startRecording(); }, [startRecording]); // 3. 녹음을 끝내면 홈으로 이동한다 const handleEnd = () => { endRecording(); navigateToHome(); }; // 4. 화면에 보여줄 형태로 반환한다 return { elapsedTime: formatRecordingTime(elapsedSeconds), // 65 → "01:05" handlePause: pauseRecording, handleEnd, }; }; 주석 순서대로 따라가 보면 흐름이 보입니다. 공통 훅 useRecording 에서 녹음 상태 조작 함수를, useNavigateToHome 에서 홈 이동 함수를 가져오고 화면에 들어오면 녹음을 시작하고 끝내기 버튼을 누르면 녹음을 멈춘 뒤 홈으로 이동하고 65 같은 초 단위 숫자를 "01:05" 같은 문자열로 바꿔 화면에 건넵니다. 하나하나 필요한 동작이고, 코드도 자연스럽게 읽혔습니다. 그런데 책에서 배운 ‘책임’을 떠올리니 궁금해졌어요. 시간 형식 바꾸기, 버튼 동작 연결하기, 종료 후 이동하기까지. 이 훅은 적어도 세 가지 일을 하고 있으니까요. 이렇게 여러 일을 하는 훅을, 하나의 책임을 가진 훅이라고 볼 수 있을까요? 제 머릿속의 ‘훅은 하나의 일만 해야 한다’ 는 공식대로라면, 쪼개야 할 훅이었죠. 그래도 바로 단정하기보다는, 여러 동작을 굳이 한곳에 둔 이유부터 묻고 싶었습니다. 그래서 팀원의 코드에 리뷰 코멘트 로 이렇게 질문했어요. “훅의 책임을 꼭 하나로 제한하기보다, 하나의 역할을 수행하기 위해 여러 책임을 모아둔 걸로 이해하면 될까요?” 같은 기능에 속하면 응집도가 높은 걸까요? 질문을 남겨두고도 생각은 계속 이어졌어요. ‘하나의 역할을 위해 모아뒀다’는 건, 결국 같은 기능에 속한다는 뜻이잖아요. 그런데 같은 기능에 속하기만 하면 응집도가 높은 걸까요? 답을 기다리는 동안, 『오브젝트』의 예제로 먼저 확인해봤어요. 책의 영화 예매 시스템에는 두 종류의 할인 조건이 나와요. 순번 조건 : 하루 중 몇 번째 상영인지로 판단 (예: 첫 회 상영) 기간 조건 : 요일과 시간대로 판단 (예: 월요일 10시~12시) 책에서 처음 설계한 DiscountCondition 은 이 두 조건을 한 클래스에서 판단합니다. 세부 구현을 조금 덜어내면 다음과 같아요. public class DiscountCondition { private DiscountConditionType type; // 순번 조건인지, 기간 조건인지 private int sequence; // 순번 조건에서만 사용 private DayOfWeek dayOfWeek; // 기간 조건에서만 사용 private LocalTime startTime; // 기간 조건에서만 사용 private LocalTime endTime; // 기간 조건에서만 사용 public boolean isSatisfiedBy(Screening screening) { if (type == DiscountConditionType.PERIOD) { return isSatisfiedByPeriod(screening); } return isSatisfiedBySequence(screening); } // 순번 할인 규칙 private boolean isSatisfiedBySequence(Screening screening) { return sequence == screening.getSequence(); } // 기간 할인 규칙 private boolean isSatisfiedByPeriod(Screening screening) { // 상영 요일이 같고, 상영 시각이 startTime ~ endTime 사이인지 확인하는 코드 (복잡해서 생략) ... } } 처음엔 이렇게 생각했어요. “둘 다 ‘할인 조건’이니까, 한 클래스에 잘 모여 있는 것 아닌가?” 하지만 책에서는 무엇 때문에 이 코드를 고치게 되는지 를 기준으로 이 클래스를 봅니다. 메서드 수정하게 되는 이유 isSatisfiedBy() 새로운 할인 조건 종류 추가 isSatisfiedBySequence() 순번 할인 규칙 변경 isSatisfiedByPeriod() 기간 할인 규칙 변경 순번 조건은 sequence 만, 기간 조건은 dayOfWeek , startTime , endTime 만 사용합니다. 순번 규칙이 바뀌어도 기간 조건은 그대로고, 기간 규칙이 바뀌어도 순번 조건은 그대로예요. ‘할인 조건’이라는 이름은 같지만, 서로 따로 바뀌는 규칙과 데이터가 한 클래스에 모여 있는 것 입니다. 책이 이 클래스의 응집도가 낮다고 말하는 이유예요. 같은 이름 아래 모여 있다고 응집도가 높은 건 아니었습니다. 응집도는 앞에서 적어둔 대로 “같은 이유로 함께 바뀌는가?” 의 관점에서 봐야 했죠. 로딩 화면과 빈 결과 화면은 같은 이유로 바뀔까요? 프론트엔드에서 자주 쓰는 코드에도 같은 질문을 던져봤습니다. 문서 검색 결과를 보여주는 컴포넌트예요. function DocumentSearchResult() { const { isPending, isError, data } = useSearchDocuments(); if (isPending) return <p>문서를 찾는 중입니다.</p>; if (isError) return <p>문서를 불러오지 못했습니다.</p>; if (data.length === 0) return <p>검색 결과가 없습니다.</p>; return <DocumentList documents={data} />; } 네 분기 모두 ‘검색 결과 화면’이라는 공통점이 있습니다. 그런데 바뀌는 이유를 따라가 보면 이야기가 달라져요. 로딩 화면 디자인이 바뀌면 → 이 컴포넌트를 연다 에러 안내 문구가 바뀌면 → 또 이 컴포넌트를 연다 검색 결과가 없을 때의 안내가 바뀌면 → 또 이 컴포넌트를 연다 서로 관계없는 이유로 같은 코드를 고치게 되니, 이 코드는 응집도가 낮다고 봤어요. DiscountCondition 이 두 규칙을 한곳에서 직접 구현하던 모습과 닮았습니다. 그런데 마지막 목록 화면은 조금 다릅니다. 목록을 어떻게 그릴지는 DocumentList 에 맡겼기 때문에, 목록 디자인이 바뀌어도 이 컴포넌트는 그대로예요. 그렇다면 나머지 화면도 각자의 컴포넌트에 맡기면 어떨까요? if (isPending) return <DocumentSearchLoading />; if (isError) return <DocumentSearchError />; if (data.length === 0) return <DocumentSearchEmpty />; return <DocumentList documents={data} />; 이제 이 컴포넌트에 남은 일은 지금 상태에 맞는 화면을 고르는 것 하나입니다. 여전히 네 가지 경우를 다루지만, 어느 화면의 디자인이 바뀌어도 이 코드는 그대로예요. 각 화면을 어떻게 그리는지는 맡은 컴포넌트 안에 감춰져 있으니, 디자인이 바뀌어도 그 변경이 여기까지 번지지 않는 거죠. 앞에서 본 캡슐화가 응집도를 지켜주는 모습이에요. 여기서 중요한 구분 하나를 얻었습니다. 여러 일의 세부 구현까지 모두 떠안는 것과, 조합만 하고 실제 일은 맡기는 것은 다르다. 스스로 하는 일과 맡기는 일 이제 이 구분을 들고 useRecorderBar 로 돌아가 볼게요. 사실 이 훅도 서로 다른 이유로 바뀝니다. 시간 표시를 01:05 에서 1분 5초 로 바꾸는 요구사항 → formatRecordingTime 과 관련 종료 후 홈이 아닌 다른 화면으로 보내는 요구사항 → handleEnd 와 관련 이것만 보면 DiscountCondition 처럼 응집도가 낮아 보여요. 하지만 앞에서 얻은 구분대로라면, 먼저 확인해야 할 게 있습니다. 이 훅은 이 일들의 세부 구현까지 떠안고 있을까요, 조합만 하고 실제 일은 각자에게 맡기고 있을까요? 코드를 다시 읽으며, 각 줄이 일을 맡기는지 , 직접 결정하는지 를 주석으로 표시해봤어요. export const useRecorderBar = () => { // 맡김: 녹음 상태와 조작은 useRecording에게 const { elapsedSeconds, startRecording, pauseRecording, endRecording } = useRecording(); // 맡김: 실제 화면 이동은 useNavigateToHome에게 const { navigateToHome } = useNavigateToHome(); // 결정: 이 화면에 들어오면 녹음을 시작한다 useEffect(() => { startRecording(); }, [startRecording]); // 결정: 녹음을 끝내면 홈으로 간다 const handleEnd = () => { endRecording(); navigateToHome(); }; return { // 맡김: 시간 형식은 formatRecordingTime에게 elapsedTime: formatRecordingTime(elapsedSeconds), handlePause: pauseRecording, handleEnd, }; }; 맡김 이라고 적은 곳부터 볼게요. useRecorderBar 는 녹음 상태를 직접 바꾸지 않습니다. useRecording() 에서 받은 endRecording() 을 부를 뿐이죠. 경과 시간도 직접 계산하지 않습니다. 전달받은 elapsedSeconds 를 쓸 뿐이에요. 반대로 결정 이라고 적은 곳이 이 훅이 직접 정하는 부분입니다. 화면에 들어오면 녹음을 시작하고, 끝내면 홈으로 간다. 결국 이 훅이 정하는 건 “이 화면에서 녹음이 어떻게 흘러가는가” , 그 흐름 하나예요. 컴포넌트가 “이제 녹음 시작해”라고 지시하지 않아도, 화면에 들어오면 훅이 알아서 녹음을 시작하죠. 자율적인 훅은 자기 일은 스스로 한다 고 했죠. 바로 이 부분이에요. 시간 표시도 마찬가지입니다. formatRecordingTime(elapsedSeconds) 라는 이름만 봐도 무슨 일을 하는지 알 수 있죠. 초를 분으로 바꾸고 앞에 0 을 붙이는 방법까지 이 훅에서 읽을 필요는 없어요. 이 차이는 코드를 고칠 때 드러납니다. 표시 형식이 바뀌면 → formatRecordingTime 안만 고치면 된다. useRecorderBar 는 그대로. 종료 후 이동할 화면이 바뀌면 → handleEnd 를 고친다. 녹음 바의 흐름을 정하는 곳이기 때문에. 그래서 useRecorderBar 에 함수 호출이 여러 개 있다는 것만으로 응집도가 낮다고 말하긴 어려웠습니다. 동작을 연결할 뿐, 각 동작을 어떻게 처리할지는 해당 함수와 훅에 맡기고 있으니까요. 그리고 마침, 기다리던 리뷰에도 답변이 달렸어요. 답변 에서 팀원은 이렇게 설명했어요. 작은 책임으로 나누더라도 결국 어딘가에서는 다시 조합해야 하고, useRecorderBar 는 녹음 바가 녹음을 보여주고 조작할 수 있도록 필요한 동작을 연결하는 자리라고요. 코드를 읽으며 제가 확인한 것과 같은 이야기였죠. 처음의 저는 일이 몇 개인지만 세고 있었는데, 그 일들을 이 훅이 떠안고 있는지, 맡기고 있는지 부터 봐야 했던 거예요. 결국 봐야 할 건 두 가지였어요. 이 훅을 고치게 만드는 이유는 무엇이고, 서로 다른 이유가 얼마나 있는가? → 응집도 함께 쓰는 각 동작의 구현이 잘 감춰져 있는가? → 캡슐화 몇 개의 일을 하느냐보다, 그 일을 떠안느냐 맡기느냐 가 더 중요한 질문이었습니다. 정보를 가장 잘 아는 곳에 맡기기 그렇다면 무엇을, 누구에게 맡겨야 할까요? 『오브젝트』 5장은 그 첫 번째 원칙으로 정보 전문가(INFORMATION EXPERT) 패턴 을 소개합니다. 책임은 그 일에 필요한 정보를 가장 잘 아는 객체에 맡긴다. 그리고 스스로 할 수 없는 일은 메시지를 보내 다른 객체에 부탁한다. 그 메시지는 다시 받는 쪽의 책임이 되고요. 앞에서 맡김 과 결정 으로 표시한 코드를 이 기준으로 다시 정리하면 다음과 같아요. 책임 필요한 정보 맡은 곳 화면에 들어오면 녹음 시작, 끝나면 홈으로 녹음 바 화면의 흐름 useRecorderBar 녹음 시작 / 정지 / 종료 앱 전체의 녹음 상태 useRecording 경과 시간을 01:05 형식으로 바꾸기 시간 표시 규칙 formatRecordingTime 홈 화면으로 이동하기 라우터 사용법 useNavigateToHome useRecorderBar 는 녹음 상태도, 라우터도 직접 알지 못합니다. 대신 녹음 바에서 무엇이 어떤 순서로 일어나야 하는지 는 누구보다 잘 알죠. 그래서 그 흐름만 직접 책임지고, 나머지는 endRecording() , navigateToHome() 이라는
То, что RADAR обнаружил и классифицировал для этой возможности. Это опубликованный источником текст, а не подтверждение, что предложение ещё действует.
스스로 일하고, 함께 일하는 훅. 『오브젝트』 공부를 시작하고 객체의 자율성과 캡슐화, 책임과 협력에 대해 배우고 있습니다. 프로젝트 코드를 작성하고 팀원들과 치열하게 리뷰를 주고받다 보니, 책에서 읽은 내용을 자연스럽게 우리가 사용하는 컴포넌트와 훅에 대입해보게 됐어요. 그 과정에서 자주 떠오른 질문이 있습니다. 훅은 정말 하나의 일만 해야 할까요? 흔히 단일 책임 원칙(SRP)을 ‘하나의 모듈은 하나의 일만 해야 한다’는 뜻으로 이해하곤 하죠. 그 말대로라면 훅도 하나의 일만 해야 할 것 같지만, 실제 훅을 열어보면 상태를 읽는 코드도, 값을 계산하는 코드도, 다른 훅을 부르는 코드도 함께 들어 있습니다. 어디까지 함께 두고 어디서 나눠야 할지, 막상 판단하려면 쉽지 않죠. 이 글은 그 답을 찾아가는…
Открыть источник