티스토리 뷰

안녕하세요. Seller & SD Engineering 팀의 개발자 김민우입니다.

 

저는 주로 백엔드 개발을 담당하고 있지만, 최근에 프론트엔드 개발, 특히 React로 캘린더 컴포넌트 만들기에 도전했습니다. 이 글에서는 백엔드 개발자의 시각에서 본 프론트엔드 개발의 독특한 점들과 더불어 Props Drilling 문제를 해결한 경험을 공유하고자 합니다. 프론트엔드 개발에 대한 저의 경험은 전문적인 관점이 아닐 수 있지만, 이 분야에 대한 새로운 시각을 제공하려 합니다.

 

배경 :

  저희 팀은 새로 오픈 할 ESM 개편안 중 '통계' 작업을 맡았습니다. 본래는 통계 페이지들의 특성상 상단에는 단순 조건 필터링, 하단에는 해당 조건에 따른 결과(그래프, 테이블 등)로 단순하게 구성하기 때문에 프론트에 대한 리소스가 적을 것을 예상되었습니다. 게다가 어려운 컴포넌트들은 전문 프론트엔드 인력이 있는 팀에서 공통 컴포넌트용 라이브러리를 제공해주셨기 때문에 굳이 프론트 전문 리소스가 해당 프로젝트에서는 배정되지 않았습니다. 그래서 쉬울 줄만 알았던 프론트엔드 작업 총괄을 백엔드 개발자인 제가 맡게 되었습니다.

 

그러나 문제는 UI/UX 요구 사항의 변화로 기존 공통 컴포넌트 라이브러리의 한계를 마주했습니다.

 

공통컴포넌트 캘린더

 

통계용 캘린더

 

  인풋 박스의 변경, 모달 내 헤더에 탭 추가, 달력 타이틀 및 하단 헤더의 변화와 같은 요소들이 추가되었고, 이는 단순한 HTML 추가를 넘어서 다른 엘리먼트와의 복잡한 상호작용을 요구했습니다.

 

 

월 단위 캘린더

 

  더욱이, '일/주/월' 별로 다르게 표시되고 작동하는 월 단위 캘린더, 그리고 시작점과 종료점을 선택하는 range 모드와 단일 날짜를 선택하는 single 모드를 구현해야 했습니다. 이 모든 요구 사항을 충족하는 6개의 새로운 컴포넌트 개발이 필요했습니다. 하지만 이런 캘린더 구조는 저희 도메인에서만 쓰는 특이한 구조였기 때문에 공통 컴포넌트 라이브러리에 요청해서 해결될 일은 아니었습니다. 라이브러리가 개발 공수 절감의 장점이 있었음에도 불구하고, 필요한 커스터마이징을 할 수 없었기 때문입니다. 백엔드 개발에서처럼 로직을 오버라이딩하거나 확장하는 방식으로는 프론트엔드 컴포넌트를 수정할 수 없었습니다.

 

  결국 캘린더와 같은 복잡한 컴포넌트들은 공통 컴포넌트 라이브러리는 깔끔하게 물거품이 되었습니다. 결과적으로, 복잡한 기능을 가진 새로운 컴포넌트를 직접 개발해야만 하는 상황에 처하게 되었습니다. 일촉즉발의 위기상황. 백엔드 개발자의 통계용 캘린더 컴포넌트 만들기 대작전은 그렇게 시작됩니다...

 

1. 리액트 단기 속성으로 이해하기 :

  서버 랜더링에 익숙한 백엔드 개발자들은 기존 웹 개발 방식을 다음 그림과 같이 알고 있을 겁니다. . 예를 들어, 스프링 부트와 같은 백엔드 프레임워크를 사용할 때, API 요청(@RestController)과 프론트 페이지(@Controller)의 구성 및 요청에 중점을 두고, 동적인 데이터를 HTML에 삽입하는 방식을 활용합니다.

 

 

  다음은 백오피스에서도 자주 쓰이는 UI/UX 구성으로 실제 테스트 가능한 구현물입니다.

 

  검색 조건들을 필터로 걸고 검색버튼과 결과값을 시각화하는 구조가 저희가 만들어야할 통계 페이지와 유사합니다. 

 

 

수도가 궁금한 국가의 이름을 영어로 검색해 보세요

Country Capital

 

 

  핵심은 html을 다 그린 이후에 javascript에서 document.getElementById와 같이 요소를 뒤늦게 찾아 기능을 '부여'한다는 점입니다. 아래는 코드입니다. 

 

import React, { useState, useEffect } from 'react';

export const CapitalSearch = () => {
    const [inputValue, setInputValue] = useState('');
    const [countries, setCountries] = useState([]);
    const [triggerFetch, setTriggerFetch] = useState(false);

     const fetchCountries = async (inputValue, setCountries) => {
	        try {
	            if (inputValue) {
	                const response = await fetch('https://restcountries.com/v3.1/all');
	                const data = await response.json();
	                const filteredCountries = data.filter(country =>
	                    country.name.common.toUpperCase().includes(inputValue.toUpperCase())
	                ).slice(0, 5);
	                setCountries(filteredCountries);
	            }
	        } catch (error) {
	            console.error("There was an error fetching the data:", error);
	        }
	    };
	
	    useEffect(() => {
	        const fetchData = async () => {
	            if (triggerFetch) {
	                await fetchCountries(inputValue, setCountries);
	                setTriggerFetch(false);
	            }
	        };
	
	        fetchData();
	    }, [triggerFetch, inputValue]);
    const handleKeyPress = (e) => { if (e.key === 'Enter') { setTriggerFetch(true); } };
    return (
        <div className="container">
        <h4>수도가 궁금한 국가의 이름을 영어로 검색해 보세요</h4>
            <SearchInput
                value={inputValue}
                onChange={(e) => setInputValue(e.target.value)}
                onKeyPress={handleKeyPress}
            />
            <SearchButton onClick={() => setTriggerFetch(true)} />
            <CountriesTable countries={countries} />
        </div>
    );
};

const SearchInput = ({ value, onChange, onKeyPress }) => (
    <input
        type="text"
        value={value}
        placeholder="Search for countries.."
        onChange={onChange}
        onKeyPress={onKeyPress}
    />
);

const SearchButton = ({ onClick }) => (
    <button onClick={onClick}>Search</button>
);

const CountriesTable = ({ countries }) => (
    <table>
        <thead>
            <tr>
                <th style={{ width: '60%' }}>Country</th>
                <th style={{ width: '40%' }}>Capital</th>
            </tr>
        </thead>
        <tbody>
            {countries.map(country => (
                <tr key={country.cca3}>
                    <td>{country.name.common}</td>
                    <td>{country.capital[0]}</td>
                </tr>
            ))}
        </tbody>
    </table>
);

 

   하지만 리액트를 사용하는 환경에서는 백엔드와 프론트엔드가 명확하게 분리됩니다. 이 경우, 백엔드 서버는 주로 API를 처리하고, 리액트는 이 서버로부터 데이터를 받아 사용자 인터페이스를 구성합니다. 브라우저는 이를 받아 화면을 직접 구성하게 되는데, 이 과정에서 화면의 각 'html 요소'들은 독립적인 변수와 기능을 가지며, 이를 '컴포넌트'라고 부릅니다.

 

 

 

  '변수와 기능? 그거 클래스 아닌가?' 라고 생각하신 백엔드 개발자 분들, 맞습니다. 백엔드 개발자들이 '변수와 기능'을 클래스와 연관지어 생각하는 것처럼, 리액트의 초기 컴포넌트 개발 방식은 React Component를 상속받은 클래스 기반 객체로 구성되었습니다. 이 접근 방식은 백엔드 개발자들에게 익숙한 객체지향 프로그래밍의 개념을 프론트엔드 컴포넌트 개발에 적용한 것입니다. 이제 더 이상 document에서 element를 뒤늦게 셀렉터로 찾아 셋팅하는 게 아닙니다. 과거에는 document에서 element를 셀렉터로 찾아 세팅하는 방식이 일반적이었지만, React의 도입으로 선언적인 컴포넌트 기반의 접근방식이 유행하기 시작한 것입니다.과거에는 document에서 element를 셀렉터로 찾아 세팅하는 방식이 일반적이었지만, React의 도입으로 선언적인 컴포넌트 기반의 접근방식이 유행하기 시작한 것입니다.

 

import React, { Component } from 'react';

class CapitalSearch extends Component {
    constructor(props) {
        super(props);
        this.state = {
            inputValue: '',
            countries: [],
            triggerFetch: false
        };
    }

    componentDidUpdate(prevProps, prevState) {
        if (this.state.triggerFetch && this.state.inputValue) {
            fetch('https://restcountries.com/v3.1/all')
                .then(response => response.json())
                .then(data => {
                    const filteredCountries = data.filter(country => 
                      country.name.common.toUpperCase().includes(this.state.inputValue.toUpperCase())
                    ).slice(0, 5);
                    this.setState({ countries: filteredCountries, triggerFetch: false });
                })
                .catch(error => {
                    console.error("There was an error fetching the data:", error);
                    this.setState({ triggerFetch: false });
                });
        }
    }

    render() {
        return (
            <div>
                <SearchInput
                    value={this.state.inputValue}
                    onChange={(e) => this.setState({ inputValue: e.target.value })}
                    onKeyPress={(e) => {
                        if (e.key === 'Enter') {
                            this.setState({ triggerFetch: true });
                        }
                    }}
                />
                <SearchButton onClick={() => this.setState({ triggerFetch: true })} />
                <CountriesTable countries={this.state.countries} />
            </div>
        );
    }
}

class SearchInput extends Component {
    render() {
        return (
            <input
                type="text"
                value={this.props.value}
                placeholder="Search for countries.."
                onChange={this.props.onChange}
                onKeyPress={this.props.onKeyPress}
            />
        );
    }
}

class SearchButton extends Component {
    render() {
        return (
            <button onClick={this.props.onClick}>Search</button>
        );
    }
}

class CountriesTable extends Component {
    render() {
        return (
            <table>
                <thead>
                    <tr>
                        <th>Country</th>
                        <th>Capital</th>
                    </tr>
                </thead>
                <tbody>
                    {this.props.countries.map(country => (
                        <tr key={country.cca3}>
                            <td>{country.name.common}</td>
                            <td>{country.capital[0]}</td>
                        </tr>
                    ))}
                </tbody>
            </table>
        );
    }
}

export default CapitalSearch;

 

 

  클래스 기반 컴포넌트에서 함수형 컴포넌트로의 전환을 통해, 코드가 간략화되고 메모리 사용이 최적화됩니다. 함수형 컴포넌트에서는 state가 클래스의 인스턴스 변수와 유사한 역할을 하며, useEffect와 같은 함수들이 변수들의 상태 변화를 감지합니다. props는 생성자의 매개변수와 유사하게 작동하며, return 아래에서는 state, props, effect의 속성에 따라 백엔드에서 template이 하는 것과 같이 HTML을 그려 나갑니다. 온라인 리액트 환경에서 해당 코드를 볼 수 있는 링크는 https://playcode.io/1628036 입니다.

 

import React, { useState, useEffect } from 'react';

export const CapitalSearch = () => {
    const [inputValue, setInputValue] = useState('');
    const [countries, setCountries] = useState([]);
    const [triggerFetch, setTriggerFetch] = useState(false);

    useEffect(() => {
        if (triggerFetch) {
            // Inlining fetchAndFilter
            if (inputValue) {
                fetch('https://restcountries.com/v3.1/all')
                    .then(response => response.json())
                    .then(data => {
                        const filteredCountries = data.filter(country => 
                          country.name.common.toUpperCase().includes(inputValue.toUpperCase())
                        ).slice(0, 5);
                        setCountries(filteredCountries);
                    })
                    .catch(error => {
                        console.error("There was an error fetching the data:", error);
                    });
            }
            setTriggerFetch(false);
        }
    }, [triggerFetch, inputValue]);

    return (
        <div className="container">
            <SearchInput
                value={inputValue}
                onChange={(e) => setInputValue(e.target.value)} // Inlining handleInputChange
                onKeyPress={(e) => {
                    if (e.key === 'Enter') {
                        setTriggerFetch(true);
                    }
                }}
            />
            <SearchButton onClick={() => setTriggerFetch(true)} />
            <CountriesTable countries={countries} />
        </div>
    );
};

const SearchInput = ({ value, onChange, onKeyPress }) => (
    <input
        type="text"
        value={value}
        placeholder="Search for countries.."
        onChange={onChange}
        onKeyPress={onKeyPress}
    />
);

const SearchButton = ({ onClick }) => (
    <button onClick={onClick}>Search</button>
);

const CountriesTable = ({ countries }) => (
    <table>
        <thead>
            <tr>
                <th style={{ width: '60%' }}>Country</th>
                <th style={{ width: '40%' }}>Capital</th>
            </tr>
        </thead>
        <tbody>
            {countries.map(country => (
                <tr key={country.cca3}>
                    <td>{country.name.common}</td>
                    <td>{country.capital[0]}</td>
                </tr>
            ))}
        </tbody>
    </table>
);

 

  React에서, 상위 컴포넌트(예: 페이지)는 상태 관리를 주로 담당합니다. 이는 백엔드의 Controller와 Service와 유사한 역할로, 데이터 흐름과 비즈니스 로직을 중앙에서 관리하게 됩니다.

State는 상위 컴포넌트에서 선언되어 하위 컴포넌트로 props를 통해 전달됩니다. 예를 들어, 페이지에서 [state, setState]를 선언하고, 하위 컴포넌트가 이를 읽거나 변경할 필요가 있다면, 페이지의 state나 setState를 props로 전달합니다. 단, 하위 컴포넌트 내부에서만 사용되는 state는 해당 컴포넌트 내에서 선언할 수 있으며, 이 경우에도 상위 컴포넌트와 같은 방식으로 관리합니다. 하지만 페이지에서 정의한 상수값은 단방향으로 하위 컴포넌트에 props로 전달됩니다.

 

캘린더 컴포넌트는 SearchInput과 유사한 역할을 하며, 상위 컴포넌트에서 API 호출 시 필요한 날짜 범위와 같은 파라미터를 state로 관리합니다. 캘린더 내부에서 setState를 통해 페이지의 state를 변경하면, 이에 따라 리렌더링되고, 이는 다시 하위의 Table 컴포넌트가 state에 따라 결과값을 새롭게 표시하게 합니다. 상수 값은 페이지에서 props로 직접 전달됩니다.

 

  저희의 요구사항에 따라 page에서 정하고 캘린더 컴포넌트로 넘겨야할 값들을 정리해보자면 다음과 같습니다.

 

상수로 선언해서 props로 넘길 것들

 캘린더 컴포넌트 생성 시에만 읽고 그 이후 변동 없음.
page에서 state를 다루고 props로 넘겨야할 것들

 API를 받고 혹은 캘린더와의 상호작용 후에 변동되는 값들을 읽기 전용으로 다시 보낼때
setState로 props로 넘겨야할 것들

 캘린더 내에서 선택하면 page에 다시 영향을 줘야하는 것들.
일/주/월 Unit, Range/Single 모드  최대 선택가능일(먼저 api를 받고 결과값에 따라 달라짐), 최소 선택가능일, 랜더링 당시 초기 시작일, 랜더링 당시 초기 종료일 시작일(월) 설정, 끝일 설정하는 함수

 

 

 

2. 캘린더 퍼블리싱 확인 후 리액트 컴포넌트 구성화 하기:

 

  통계용 캘린더를 구현할 때, React를 활용해 객체지향적인 접근을 취했습니다. 이를 위해, 변수와 기능을 포함할 가능성이 높은 Element들을 중심으로 컴포넌트화하는 것을 우선시했습니다. 다음은 html과 css만 적용된 퍼블리싱 스크린샷입니다.

 



  단순한 CSS 스타일링을 위한 wrapper와 같은 엘리멘트는 별도의 컴포넌트로 분리하지 않기로 결정했습니다. 대신, props에 따라 변화하거나 다르게 작동해야 하는 엘리멘트들을 중심으로 컴포넌트를 구성했습니다. 이러한 접근으로 구성된 컴포넌트 구조는 다음과 같습니다: 컴포넌트의 계층은 노란색 > 빨간색 > 보라색 > 파란색 > 초록색 순서로 나타납니다.

 

 

 

앞서 언급했듯이, 캘린더 컴포넌트는 여러 곳에서 재사용될 필요가 있습니다. 이를 위해, 캘린더의 구성 요소들, 예를 들어 Table, Group, Title, Header, Cell 등도 재사용 가능해야 합니다. 이러한 요소들은 각각 독립적인 작은 컴포넌트로 제작되어야 합니다.

하지만, 저희의 통계용 캘린더는 단순한 '일' 단위 캘린더뿐만 아니라 '주'와 '월' 단위 캘린더도 포함해야 합니다. 이 때문에, 각 단위별 캘린더는 HTML 구조가 다르기 때문에 별도로 구현해야 합니다.

 

 

캘린더 컴포넌트의 구조를 유지하면서, 필요한 서브 컴포넌트들을 적절하게 배치합니다. 

 

 

이 과정에서 중요한 것은 재활용 가능한 컴포넌트들을 필요한 만큼 세밀하게 나누는 것입니다. 각 컴포넌트는 상위 컴포넌트로부터 받은 props를 기반으로 렌더링됩니다.

 

 

 

한편, 페이지로부터 직접 관리될 필요가 없는 독립적인 변수들은 각 컴포넌트 내에서 자체 state로 관리됩니다. 이러한 방식으로, 각 컴포넌트의 독립성과 재사용성을 효과적으로 보장합니다.

 

 

3. Props Drilling을 해결하고 여러 개 재활용 가능한 컴포넌트로 완성시키기 :

 

  컴포넌트를 여러 계층으로 세분화하여 구성했습니다. 그러나 이 구조에서는 'Props Drilling'이라는 문제가 발생합니다. 예를 들어, 가장 하단의 Cell 컴포넌트가 상단의 Calendar 컴포넌트로부터 props를 받아야 할 경우, Calendar > Modal > Group > Table > Cell과 같은 중간 컴포넌트 모두가 해당 props를 전달해야 합니다. 이는 특히 필요한 props가 여러 개일 때 더욱 복잡해집니다. 또한, Cell에서 발생하는 이벤트가 상단 컴포넌트에 영향을 미쳐야 한다면, setState 함수도 props로 전달되어야 합니다.

 

백엔드 개발에서 이 문제는 상위 클래스에서 최하위 클래스까지 변수를 계속 전달해야 하는 상황과 유사합니다.

 

public class MainClass {
    public static void main(String[] args) {
        String message = "Hello from MainClass";
        ClassA classA = new ClassA(message);
        classA.processMessage();
    }
}

class ClassA {
    private String message;
    private ClassB classB;

    public ClassA(String message) {
        this.message = message;
        this.classB = new ClassB(message);
    }

    public void processMessage() {
        classB.processMessage();
    }
}

class ClassB {
    private String message;
    private ClassC classC;

    public ClassB(String message) {
        this.message = message;
        this.classC = new ClassC(message);
    }

    public void processMessage() {
        classC.processMessage();
    }
}

class ClassC {
    private String message;

    public ClassC(String message) {
        this.message = message;
    }

    public void processMessage() {
        System.out.println("ClassC received: " + message);
    }
}

 

1) Jotai Atom

 

  Jotai는 전역 상태 관리에 유용한 라이브러리입니다. 필요한 state나 setState는 별도의 파일에서 전역 변수처럼 관리하고, 해당 변수들은 useAtom을 통해 필요한 컴포넌트에서 적절하게 사용합니다. Spring으로 따지자면 상태를 빈 처럼 ApplicationContext에 등록하고 쓰고 싶을 때는 Spring DI를 사용해 직접 주입하는 것과 유사한 전역 상태 관리 방식인 셈입니다. 

 

 

 

위 코드처럼 파일을 따로 두어 필요한 atom을 생성해두면, 아래 코드처럼 딱 써야할 컴포넌트에서 useAtom을 통해서 import 하듯이 적재적소로 사용하는 것이 가능하니, props drilling을 쉽게 해결할 수 있는 셈입니다.

 

 

하지만 Jotai 사용 시 몇 가지 문제점이 있습니다:

•  Jotai Atom은 초기에 상수 값을 필수로 사용합니다.


  캘린더를 초기화할 때, API로부터 받은 최대 선택 가능 일자(maxDate, minDate)와 시작일, 종료일을 설정해야 합니다. Jotai Atom은 초기에 상수 값을 요구하기 때문에, 더미 데이터를 넣은 후 변경해야 합니다. 이는 초기 랜더링 시 불필요한 랜더링을 초래할 수 있습니다. 이 문제는 atomFamily를 사용하여 매개변수로 값을 받을 수 있게 하여 해결했습니다.

 


•  Atom들은 '전역'변수로 활용된다.

 

  Jotai 라이브러리에서 사용되는 Atom들은 전역 변수로 활용됩니다. 이는 Atom을 변경할 때 해당 Atom을 사용하는 모든 캘린더에 영향을 줄 수 있는 문제점을 내포합니다. 예를 들어, 하나의 Atom만을 사용하여 여러 캘린더를 구성한다면, 한 캘린더가 열릴 때 모든 캘린더가 동시에 열리는 상황이 발생할 수 있습니다. 이는 캘린더 개수만큼 Atom을 별도로 선언해야 한다는 문제를 야기합니다.

 

atomFamily에 각 캘린더에 고유한 ID를 넣으면 ID별로 Atom을 동적으로 생성하는 데 사용됩니다. 이 방식은 각 캘린더가 독립적인 상태를 가질 수 있게 해줍니다. 즉, atomFamily를 사용하면 같은 상태를 공유하는 여러 캘린더 대신 각 캘린더마다 고유한 상태를 가질 수 있는 Atom을 생성할 수 있습니다.

게다가 Provider를 사용하는 경우, Atom들을 특정 범위 내에서만 동작하게 제한할 수 있습니다. 이는 Atom이 더 이상 전역 변수처럼 작동하지 않고, Provider로 감싸진 범위 내에서만 독립적인 상태를 유지한다는 의미입니다. 따라서, 각 캘린더에 대한 상태 관리가 특정 컴포넌트 범위 내에서만 이루어지게 되어, 다른 캘린더와의 상태 간섭을 방지할 수 있습니다.

이러한 방식으로, 각 캘린더가 독립적인 상태를 가질 수 있게 하여, 전역 상태 관리의 복잡성을 줄이고, 캘린더 간의 상호 작용 문제를 해결할 수 있습니다.

 

 

 

 

  그러나 독립적인 atom을 각 캘린더별로 주기 위해 제한하는 Provider 때문에, 역설적이게도 상위 컴포넌트의 상태에 영향을 주는 것이 어렵다는 문제가 있습니다. 이는 특히 저희가 만들고자 하는 사이트처럼, 페이지의 상태를 기반으로 다른 컴포넌트에 영향을 주어야 하는 경우에 두드러집니다. 아래 그림에 보이듯이 atom을 캘린더 내에서 제한시켰기 때문에 오히려 page, 그리고 나아가 형제 컴포넌트인 Table에도 영향을 끼치지 못하게 됩니다.

 

 

이 문제의 근원은 Atom이 페이지의 상태가 아니라 전역 상태로 관리되기 때문입니다. 처음에 설명했듯이, Atom을 파일에 별도로 관리하고, 일정 컴포넌트 내에서만 부분적으로 전역화하기 위해 Provider로 감싸는 구조를 취했습니다. 그러나 이러한 접근은 하위 컴포넌트가 상위 컴포넌트에 영향을 미칠 수 있는 방법을 제공하지 않습니다. 결과적으로, 캘린더 컴포넌트는 읽기 전용으로 제한되어 버리는 문제가 발생했습니다.

 

이러한 이유로, 최종적으로 jotai는 캘린더 컴포넌트에서 사용하지 않기로 결정했습니다.

 

 

2) React Context API

 

  Context API를 사용하는 방법으로 전환하기로 결정했습니다. Context API는 jotai나 redux와 같은 현대적인 상태 관리 라이브러리가 등장하기 전의 전통적인 React 상태 관리 방식입니다. 이 방식에서도 Provider를 사용하는 것은 동일합니다. 하지만 jotai의 Atom이나 Redux의 Store처럼 무조건 상태처럼 작동하는 대신, 일반적인 props를 사용하여 state, setState 또는 상수처럼 사용할 props를 Provider 내에서만 전역화시켜 필요한 곳에 적절하게 사용합니다. 또한 React Context API는 React의 기본 기능이기 때문에 추가적인 라이브러리나 의존성 없이 전역 상태관리를 한다는 장점이 있습니다.

 

  다른 점은, jotai의 Atom이나 Redux의 Store가 페이지와 독립적인 제3의 파일에서 관리되는 반면, Context API는 페이지 별로 상태를 관리하고 이를 props로 전달하는 방식을 취합니다. 이렇게 하면, 각 페이지에서 캘린더를 재활용할 수 있으며, 서로 다른 페이지 간에는 상태를 공유하지 않아 각 페이지의 독립성을 유지할 수 있습니다.

한 페이지 예시 1-1
한 페이지 예시 1-2

 

다른 페이지 예시 2-1

 

다른 페이지 예시 2-2

 

 Context API를 사용하면 props drilling 문제를 근본적으로 해결할 수 있습니다. 필요한 데이터는 다음 코드처럼 useContext를 통해 직접 불러올 수 있으므로, 중간 컴포넌트를 통하지 않고도 필요한 데이터에 접근할 수 있습니다. 

 

 

3) 의존성 역전 후 page와 직접적인 연결

 

  기존의 구조에서는 페이지가 주요 state를 관리하고, 이를 읽기 전용 형태로 자식 컴포넌트에 props로 넘기거나, 수정을 위해 setState를 넘겨야 했습니다.

 

 

  하지만 '의존성 역전'을 활용하면 이러한 구조를 변형시켜 props drilling 문제를 해결할 수 있습니다. 의존성 역전을 백엔드 개발에 비유한다면, 서비스 계층(Service Layer)과 데이터 액세스 계층(Data Access Layer) 간의 관계를 들 수 있습니다. 백엔드에서는 서비스 계층이 구체적인 DAO 구현체가 아닌, DAO 인터페이스에 의존함으로써, 데이터베이스 접근 방식이 변경되어도 서비스 계층의 코드 수정이 필요 없습니다. 이와 유사하게, 프론트엔드에서 상위 컴포넌트가 하위 컴포넌트의 구체적 구현에 의존하지 않고, props를 통해 데이터를 전달함으로써 유연성과 재사용성을 높일 수 있습니다. 예를 들어, DateCell과 같은 컴포넌트는 일반적으로 Calendar, Modal, Group, Table과 같은 여러 부모 컴포넌트를 거쳐야 하지만, 이를 상위 컴포넌트인 Calendar와 바로 연결시켜 계층 구조를 단순화할 수 있습니다.

 

이러한 구조에서는 props와 state의 흐름이 더욱 직관적이 되며, 상위 컴포넌트에서 props를 통해 세부 제어가 가능해집니다. 이는 페이지별로 다른 내부 로직이 필요한 경우, 특정 컴포넌트를 맞춤형으로 재구성하여 재사용할 수 있는 유연성을 제공합니다. 다음은 예제 코드입니다.

 

const Calendar = ({ dateCellComponent, footerComponent, startDate, endDate, setStartDate, setEndDate }) => {
  return (
    <div className="calendar">
      {/* Calendar Default HTML Structure */}
      <div className="calendar-modal-groups">
        {/* Modal Groups */}
      </div>
      <div className="calendar-table">
        {/* Default Table Structure */}
        {dateCellComponent ? React.cloneElement(dateCellComponent, { startDate, endDate, setStartDate, setEndDate }) :
          <DefaultDateCell startDate={startDate} endDate={endDate} setStartDate={setStartDate} setEndDate={setEndDate} />}
      </div>
      {/* Footer */}
      {footerComponent ? React.cloneElement(footerComponent, { startDate, endDate }) : <DefaultFooter />}
    </div>
  );
};

export default Calendar;

 

게다가 기본 DateCell 대신 새로운 컴포넌트를 사용하고자 할 때는 아래와 같이 Calendar 컴포넌트에 직접 전달할 수 있습니다.

 

import React, { useState } from 'react';
import Calendar from './Calendar';
import CustomDateCell from './CustomDateCell';
import CustomFooter from './CustomFooter';

const SamplePage = () => {
  const [startDate, setStartDate] = useState(null);
  const [endDate, setEndDate] = useState(null);

  return (
    <Calendar
      startDate={startDate}
      endDate={endDate}
      setStartDate={setStartDate}
      setEndDate={setEndDate}
      dateCellComponent={<CustomDateCell onSelectDate={/* 생략 */} />}
    />
  );
};

export default SamplePage;

 

  그러나 이 방식을 채택하면 현재 구조에 많은 변경이 필요하며, 개발 비용이 상당히 증가할 수 있습니다. 또한, 재사용자에게 너무 많은 자유도를 제공함으로써 개발 과정이 복잡해지고, 간단한 설정 작업이 더 어려워질 수 있다는 점도 고려해야 합니다. 다만, 만약 제가 만든 캘린더를 공통 라이브러리에 올리려고 한다면 이 방식으로 구조를 변경할 용의는 있습니다.

 

마무리 :

 

  이번 프로젝트는 백엔드 개발자로서 프론트엔드의 세계에 발을 들여놓는 중요한 계기가 되었습니다. 처음에는 단순하게 생각했던 프론트엔드 작업이 실제로는 깊이 있고 복잡한 과제를 내포하고 있었습니다. 특히, 기존의 공통 컴포넌트 라이브러리를 벗어나 새롭게 캘린더 컴포넌트를 개발하는 과정에서는 많은 도전과 학습이 필요했습니다.

 

프로젝트를 진행하며, 공간지각능력이 예상보다 중요한 역할을 했다는 점에 특히 놀랐습니다. UI 구성 요소들 간의 상호 작용과 레이아웃 배치에서 공간적인 이해가 필수적이었기 때문입니다. 이는 백엔드 개발에서의 경험이 프론트엔드 개발의 다양한 측면을 이해하는 데 큰 도움이 되었음을 보여줍니다. 또한, 사용자 인터페이스와의 직접적인 상호작용을 통해 UI/UX에 대한 이해도가 크게 향상되었습니다.

 

가장 인상 깊었던 것은 '의존성 역전'과 같은 개념을 통해 프론트엔드 개발의 유연성과 재사용성을 극대화하는 방법을 배웠다는 점입니다. 백엔드 개발에서의 경험이 이러한 개념을 이해하고 적용하는 데 큰 도움이 되었습니다. 또한, 사용자 인터페이스와 직접적으로 상호작용하는 프론트엔드 개발의 특성상, UI/UX에 대한 이해도가 크게 향상되었습니다.

이 경험을 통해, 백엔드 개발자로서의 제 시각이 한층 넓어졌다고 느낍니다. 앞으로도 이러한 다양한 경험을 통해 더욱 풍부한 개발 능력을 갖추어 나가고자 합니다. 끝으로, 이 글이 백엔드 개발자가 프론트엔드 개발에 도전하는 데 도움이 되길 바라며,전문 프론트엔드 개발자들의 훈수도 언제든 환영입니다.

댓글