[ReactNative] 국내 1위 네이버 캘린더 앱 클론 코딩을 해보자 (1)

성 우·2023년 8월 5일
0

React-Native

목록 보기
4/4

아래는 네이버 앱 메인화면 입니다.

모든 언어의 공부는 클론코딩부터 시작이다. 대학생 시절 노마드 코더나 유튜브에 돌아다니는 클론코딩을 했던 경험이 많았습니다. ReactNative 를 첫 시작하는 초보개발자분들이 있다면 오픈소스 라이브러리의 사용법이나 ReactNative가 이런것이구나 하고 익히는데 캘린더만한 앱이 없을 것 같고, 저도 집에서 천천히 복습할겸 시작하게된 프로젝트입니다.

환경

  • react-native 0.72.3
  • react-native-calendars
  • react-redux

1. 개발에 필요한 Util 함수 개발

calendarUtils.js

/**
 * @title Date 객체를 스트링 형태의 날짜로 변환해줍니다.
 * @param date
 * @returns {string}
 */
export function getFormattedDate(date) {
    const year = date.getFullYear();
    const month = String(date.getMonth() + 1).padStart(2, '0');
    const day = String(date.getDate()).padStart(2, '0');

    return `${year}-${month}-${day}`;
}


/**
 * 스트링 형태의 날짜를 주면 분리해서 json으로 만들어줌.
 * @param dateString
 * @returns {{month: *, year: *, day: *}}
 */
export function parseDateString(dateString) {
    const [year, month, day] = dateString.split('-');

    return {
        year,
        month: String(parseInt(month)), // 단일 자리 월에 0을 붙이지 않도록 변환
        day: String(parseInt(day))      // 단일 자리 일에 0을 붙이지 않도록 변환
    };
}

2. 캘린더앱의 핵심 캘린더 구현하기

가장 많이 사용하는 오픈소스 라이브러리를 사용해서 최대한 네이버 캘린더와 비슷하게 한번 만들어보려 합니다.

무려 8.6K의 깃허브 Stars!!
https://wix.github.io/react-native-calendars/docs/


2-1 메인 캘린더

사용자가 선택한 날짜는 앱 자체에서 전역적으로 관리하면서 다른 기능에도 부가적으로 사용해야될것 같아 ReduxStore에서 관리하도록 처리했습니다.

MainCalendar.js

export default function MainCalendar() {
    const dispatch = useDispatch();
    const windowWidth = Dimensions.get('window').width;


    let currentMonth;
    return (
        <>
            <CalendarHeader />
            <CalendarList
                onDayPress={day => {
                    dispatch(selectedDate(day.dateString))
                }}
                customHeader={({current})=>{
                    //현재 헤더의 년월 정보를 변수에 저장해놓고 Day컴포넌트르 만들때 사용할게요.
                    currentMonth = parseDateString(current);
                }}
                headerStyle={{height: 200}}
                dayComponent={(data) => {
                    return (
                        <Day data={data} currentMonth ={currentMonth}/>
                    )
                }}
                //월이 바뀔경우 바뀐 월의 1일을 선택합니다.
                onMonthChange={month => {
                    // console.log("바뀜")
                    const parseString = parseDateString(month.dateString);
                    dispatch(selectedDate(`${parseString.year}-${parseString.month < 10 ? `0${parseString.month}` : `${parseString.month}`}-01`))
                }}
                hideExtraDays={false}
                horizontal={true}
                pagingEnabled={true}
                disabledByDefault={true}
                calendarWidth={windowWidth}

            />
        </>
    );
}

2-2 각 일(day)영역 컴포넌트 만들기

  1. handleOnPressDay() 함수는 각 컴포넌트별로 이벤트가 모두 생성되기때문에 UseCallBack을 통해 메모이제이션 해두고 캐싱하도록 처리해 컴포넌트 성능을 개선했습니다.

  2. currentMonth는 캘린더가 첫 화면을 렌더링할때 렌더링하는 년,달 입니다.
    해당 매게변수에 저장되있는 값으로 이전달인지 다음달인지 확인해서 컴포넌트 Text Color를 결정합니다. (아래 코드 부분을 보시면됩니다.)

customHeader={({current})=>{
	 //현재 헤더의 년월 정보를 변수에 저장해놓고 Day컴포넌트르 만들때 사용할게요.
  	 //캘린더가 생성될때마다 생성되는 날짜 정보를 변수에 저장해서 사용함.
     currentMonth = parseDateString(current);
  }}

MainCalendar.js

const Day = ({ data ,currentMonth}) => {
    const todayDate = getFormattedDate(new Date());

    const dayDate = {
        year: data.date.year.toString(),
        month: data.date.month.toString(),
        day: data.date.day.toString(),
    };
    const dispatch = useDispatch();
    const selected = useSelector(state => state.selectedDate);
    //현재 선택되어있는 날짜 flag
    const isSelected = selected === data.date.dateString;
    //현재 생성하는 Day 컴포넌트의 월과 헤더 타이틀에 표시되는 월이 동일한가? 동일하지 않으면 Gray 처리 해야함.
    const isSameMonth = currentMonth.month === dayDate.month;
    //현재 생성하는 Day컴포넌트가 오늘인가? 오늘이라면 검은색으로 표시해줘야함
    const isToday = todayDate === data.date.dateString;
    //컴포넌트 Text Color
    const textColor = isSameMonth ? (isToday ? 'white' : 'black') : 'gray';

    //해당 함수는 각 컴포넌트마다 모두 똑같기 때문에 UseCallBack에 넣어놓고 사용하자.
    const handleOnPressDay = useCallback(({ date }) => {
        dispatch(selectedDate(date.dateString));
    }, [dispatch]);

    return (
        <TouchableOpacity
            activeOpacity={1}
            style={[{ height: 40, paddingLeft: 8, paddingRight: 8 }]}
            onPress={() => handleOnPressDay(data)}>
            <View
                style={[
                    styles.day,
                    isSelected && styles.selectedDay,
                    isToday && styles.today,
                ]}>
                <Text
                    style={[
                        globalFonts.fontSemiBold,
                        {
                            fontSize: 13,
                            color: textColor,
                        },
                    ]}>
                    {data.date.day}
                </Text>
            </View>
        </TouchableOpacity>
    );
};

2-3 사용자가 선택한 날짜 store에 저장하기

action.js

// Action Types
export const SELECTED_DATE = 'SELECTED_DATE';
// Action Creators
export const selectedDate = (state) => {
    return {
        type: SELECTED_DATE,
        state : state
    };
};

2-4 캘린더 헤더 만들기

1.헤더에서도 해당 년월을 보여 줘야하는 요구사항이 있어 store에 저장되어있는 선택된 날짜를 가져와 관리합니다.

const CalendarHeader = React.memo(({}) => {
    const selected = useSelector(state => state.selectedDate);
    const parts = selected.split('-');
    const year = parts[0];
    const month = parseInt(parts[1]);

    return(
        <View style = {[styles.calendarHeaderView,{height:70}]}>
            <View style = {{flex:1}}>
                <Text style = {[globalFonts.fontBold,{fontSize:20}]}>{`${year}. ${month}`}</Text>
            </View>
            <View style = {{flex:1,flexDirection:'row',alignItems:'flex-end',marginBottom:4}}>
                    <Text style = {[styles.dayOfWeek,{color:'red',marginRight:1}]}></Text>
                    <Text style = {[styles.dayOfWeek]}></Text>
                    <Text style = {[styles.dayOfWeek]}></Text>
                    <Text style = {[styles.dayOfWeek]}></Text>
                    <Text style = {[styles.dayOfWeek]}></Text>
                    <Text style = {[styles.dayOfWeek]}></Text>
                    <Text style = {[styles.dayOfWeek,{color:'blue'}]}></Text>
            </View>
        </View>
    )
})

2-5 전체 코드

import {StyleSheet, Text, TouchableOpacity, View} from 'react-native';
import React, {useState, useMemo, useEffect, useCallback} from 'react';
import {Dimensions} from 'react-native';
import {CalendarList} from "react-native-calendars";
import {globalFonts} from "../styles/globalStyles";
import {getFormattedDate, parseDateString} from '../utils/calendarUtils'
import {selectedDate, selectedPrevDate} from "../modules/actions/actions";
import {Provider, useDispatch, useSelector} from 'react-redux'


const todayDate = getFormattedDate(new Date());
export default function MainCalendar() {
    const dispatch = useDispatch();
    const windowWidth = Dimensions.get('window').width;
    let currentMonth;

    return (
        <>
            <CalendarHeader />
            <CalendarList
                onDayPress={day => {
                    dispatch(selectedDate(day.dateString))
                }}
                customHeader={({current})=>{
                    //현재 헤더의 년월 정보를 변수에 저장해놓고 Day컴포넌트르 만들때 사용할게요.
                    currentMonth = parseDateString(current);
                }}
                dayComponent={(data) => {
                    return (
                        <View style = {{height:40}}>
                            <Day data={data} currentMonth ={currentMonth}/>
                        </View>
                    )
                }}
                //월이 바뀔경우 바뀐 월의 1일을 선택합니다.
                onMonthChange={month => {
                    //오늘 날짜가 포함된 달이 아닌경우에만 1을 선택함
                    if (month.dateString !== todayDate) {
                        const parseString = parseDateString(month.dateString);
                        dispatch(selectedDate(`${parseString.year}-${parseString.month < 10 ? `0${parseString.month}` : `${parseString.month}`}-01`))

                    }
                }}
                hideExtraDays={false}
                horizontal={true}
                pagingEnabled={true}
                // disabledByDefault={true}
                calendarWidth={windowWidth}

            />
        </>
    );
}




const Day = ({ data ,currentMonth}) => {


    const dayDate = {
        year: data.date.year.toString(),
        month: data.date.month.toString(),
        day: data.date.day.toString(),
    };
    const dispatch = useDispatch();
    const selected = useSelector(state => state.selectedDate);
    //현재 선택되어있는 날짜 flag
    const isSelected = selected === data.date.dateString;
    //현재 생성하는 Day 컴포넌트의 월과 헤더 타이틀에 표시되는 월이 동일한가? 동일하지 않으면 Gray 처리 해야함.
    const isSameMonth = currentMonth.month === dayDate.month;
    //현재 생성하는 Day컴포넌트가 오늘인가? 오늘이라면 검은색으로 표시해줘야함
    const isToday = todayDate === data.date.dateString;
    //컴포넌트 Text Color
    const textColor = isSameMonth ? (isToday ? 'white' : 'black') : 'gray';

    //해당 함수는 각 컴포넌트마다 모두 똑같기 때문에 UseCallBack에 넣어놓고 사용하자.
    const handleOnPressDay = useCallback(({ date }) => {
        dispatch(selectedDate(date.dateString));
    }, [dispatch]);

    return (
        <TouchableOpacity
            activeOpacity={1}
            style={[{flex:1, minWidth:50,alignItems:'center'}]}
            onPress={() => handleOnPressDay(data)}>
            <View
                style={[
                    styles.day,
                    isSelected && styles.selectedDay,
                    isToday && styles.today,
                ]}>
                <Text
                    style={[
                        globalFonts.fontSemiBold,
                        {
                            fontSize: 13,
                            color: textColor,
                            textAlign: 'center',

                        },
                    ]}>
                    {data.date.day}
                </Text>
            </View>
        </TouchableOpacity>
    );
};



const CalendarHeader = React.memo(({}) => {
    const selected = useSelector(state => state.selectedDate);
    const parts = selected.split('-');
    const year = parts[0];
    const month = parseInt(parts[1]);

    return(
        <View style = {[styles.calendarHeaderView,{height:70}]}>
            <View style = {{flex:1}}>
                <Text style = {[globalFonts.fontBold,{fontSize:20}]}>{`${year}. ${month}`}</Text>
            </View>
            <View style = {{flex:1,flexDirection:'row',alignItems:'flex-end',marginBottom:4}}>
                    <Text style = {[styles.dayOfWeek,{color:'red',marginRight:1}]}></Text>
                    <Text style = {[styles.dayOfWeek]}></Text>
                    <Text style = {[styles.dayOfWeek]}></Text>
                    <Text style = {[styles.dayOfWeek]}></Text>
                    <Text style = {[styles.dayOfWeek]}></Text>
                    <Text style = {[styles.dayOfWeek]}></Text>
                    <Text style = {[styles.dayOfWeek,{color:'blue'}]}></Text>
            </View>
        </View>
    )
})


const styles = StyleSheet.create({
    calendarHeaderView: {
        paddingLeft: 16,
        paddingRight: 16,
        paddingTop: 12,
        // paddingBottom: 12,
        flexDirection:'column',
        // flex:1,
    },
    dayOfWeek : {
        flex:1,
        minWidth:50,
        fontSize:12,
        textAlign: 'center',
    },
    day: {
        alignItems: 'center',
        justifyContent: 'center',
        width: 22,
        height: 22,
    },
    selectedDay: {
        borderRadius: 15,
        backgroundColor: '#C9C9C9'
    },
    today: {
        borderRadius: 15,
        backgroundColor: 'black'
    }

});



2-5 완성된 화면

실제 네이버 앱


좀 비슷한가요? 시간이 된다면 하단 상세 일정 정보와 메뉴 네비게이터도 만들어서 올려보겠습니다!

profile
풀스택 개발자가 되고싶은 개발자

1개의 댓글

comment-user-thumbnail
2023년 8월 5일

좋은 글이네요. 공유해주셔서 감사합니다.

답글 달기