1. 친구 추가 시 Firestore DB에 저장

친구 찾기 기능을 이용하여 사용자 아이디를 검색하면 해당 사용자가 검색 결과로 나온다.

친구 추가하기 버튼을 클릭하면 친구 추가 진행 확인 alert가 뜨고, 확인 버튼을 클릭했을 때 Firestore DB에 저장된다.

 

 

현재 DB의 구조는 다음과 같이 되어있다.

users (컬렉션)

ㄴ사용자 이메일 (문서)

  ㄴfriends (컬렉션)

   ㄴ친구 이메일 (문서)

    ㄴ친구 아이디 (필드)

    ㄴ친구 이름 (필드)

로그인한 사용자의 문서 아래에 있는 friends 목록에 추가한 친구 정보가 등록되는 것이다.

 

이름은 추후에 수정 기능을 제공할 예정이라 필드에 넣고 싶지 않았는데

사이드바에 친구 목록을 불러오는 것을 구현할 때 중첩으로 DB를 읽어와야해서 비동기 처리가 필요한데

이것을 해결하지 못해서 나중에 다시 시도해보기로 하고, 현재는 이름 필드도 추가하게 해놨다.

 

 

2. 사이드바 친구 목록 표시

친구 추가, 그리고 나중에 구현될 친구 삭제 기능을 이용하여 친구 목록이 수정되면 친구 목록이 바로 업데이트될 수 있도록

첫 렌더링 후에 실행되는 useEffect 안에서 Firestore의 onSnapshot 메소드를 이용했다.

 

    const loginEmail = useSelector(state => state.user.email); //redux에서 관리하는 로그인한 사용자 이메일
    const [friends, setFriends] = React.useState([]);	//친구 목록을 상태로 관리함
    const usersFB = firestore.collection('users');	//Firestore에 있는 user 컬렉션

	useEffect(() => {
        usersFB.doc(loginEmail).collection('friends').onSnapshot((docs) => {
            let listFromFB = [];
            docs.forEach((doc) => {
                listFromFB.push({id: doc.data().id, name: doc.data().name, chatRoomNum: doc.data().chatRoomNum});
            });
            setFriends(listFromFB);
    })}, []);

 

DB에서 읽어온 친구 목록을 listFromFB라는 배열에 담고 이것을 상태로 넘겨주면 리렌더링 되면서 사이드바에 친구 목록이 반영된다.

 

 

친구 등록 후 사이드바에 해당 사용자가 추가된 모습이다.

 

 

3. 친구 이름 클릭 시 1:1 채팅방 팝업 띄우기

사이드바에 있는 친구 목록에서 각 사용자를 클릭하면 해당 사용자와 1:1 채팅을 할 수 있도록 채팅방 팝업을 띄운다.

리액트 라우팅에서 URL 파라미터를 이용하여 채팅 상대에 대한 정보를 전달한다.

 

 

전체적인 레이아웃 영역만 잡은 상태이고 조만간 채팅 메세지 레이아웃을 만들 예정이다.

1. 친구 검색 바로 업데이트하기

부모 컴포넌트(FindFriends.js)에서 검색할 아이디를 입력하면 props로 넘겨서

자식 컴포넌트(FriendProfile.js)가 바뀌어서 검색 결과를 보여주도록 의도했다.

 

그런데 처음 검색할 때는 제대로 출력이 되지만 두번째로 검색을 할 때는 FriendProfile.js가 바뀌지 않는 것이다.

FindFriends.js에서 검색한 아이디를 상태로 관리하기 때문에

예상대로라면 FriendProfile.js의 props 값이 바뀌므로 출력되는 내용이 바뀌어야하는데 그렇지 않았다.

 

라이프 사이클 함수를 이용하면 해결될까 싶어서 FriendProfile.js를 함수형 컴포넌트에서 클래스형 컴포넌트로 바꿔보기도 했으나 아무 변화가 없었다.

 

관련 자료를 찾아보다가 뜻밖의 해결책을 발견했다.

 

https://www.py4u.net/discuss/976613

 

React: why child component doesn't update when prop changes

Answer #11: I was encountering the same problem. I had a Tooltip component that was receiving showTooltip prop, that I was updating on Parent component based on an if condition, it was getting updated in Parent component but Tooltip component was not rende

www.py4u.net

 

 

나에게 도움이 된 답변은 Answer #6으로, key값을 추가하면 제대로 작동한다는 것이다.

그리고 이 방법대로 했더니, 정말 잘 작동되는 것이다!

리액트에서의 key값의 의미를 좀 더 찾아봐야겠다.

 

그리고 중첩 삼항 연산자를 사용할 부분이 생겨서 관련 자료를 찾아보았다.

 

https://nm-it-diary.tistory.com/38

 

[JAVA] 삼항 연산자 사용법 - 여러개 중첩으로 사용하기

if문 대신 삼항 연산자로 간단한 조건문을 구현할 수 있습니다. if문과 비교하여 삼항 연산자에 대해 알아보겠습니다. 1. if문 예제 int num = 5; String result = ""; if( num == 5 ) { result = "num은 5"; } el..

nm-it-diary.tistory.com

 

삼항 연산자는 JAVA와 JS의 문법이 같은 듯 하다.

 

 

코드 수정 이후 검색을 한번 하고 바로 이어서 다른 아이디를 검색하는 것이 가능해졌다.

그리고 검색 결과가 없을 때 메세지를 출력하도록 수정했다.

 

2. 로그아웃 시 웰컴 화면으로 이동하기

redux를 이용하여 로그인 사용자 상태를 관리하는 것으로 변경되어서

로그아웃 시 redux로 관리하는 상태를 초기화해야 웰컴 화면으로 이동하게 되었다.

 

redux 모듈에 초기값으로 상태를 변경하는 액션 생성 함수 및 액션을 추가하고

useDispatch를 이용해서 해당 액션 생성 함수를 실행시켰다.

 

redux 모듈인 user.js에서 관련 부분만 나타내면 이렇다.

//Actions
const RESET_USER = 'user/RESET_USER';

const initialState = {
    email: '',
    id: '',
    name: '',
    is_loaded: false,
}

//Action Creators
export const resetUser = () => {
    return {type: RESET_USER};
}

//Reducer
export default function reducer(state = initialState, action = {}){
    switch(action.type){
        //do reducer stuff
        case 'user/RESET_USER': {
            return initialState;
        }
        default:
            return state;
    }
}

 

useDispatch를 이용해서 resetUser 액션 생성 함수를 실행하는 부분이다.

import { useDispatch } from 'react-redux';

const Main = (props) => {
    const dispatch = useDispatch();
    
    const logout = () => {
        let popup = window.confirm('로그아웃 하시겠습니까?');
        if (popup) {    //'예'를 선택했을 때
            userSignOut();  //Firebase Authentication 로그아웃
            dispatch(resetUser());  //redux 유저 정보 초기화
            history.push('/'); //웰컴 화면으로 이동
        }
    }
    ...
}

 

1. 화면 레이아웃 잡기

웹페이지의 레이아웃을 잡기 위해 Figma로 간단히 프로토타입을 그려보았다.

 

전체적인 색감이 정해진건 아닌데 프로토타입은 핑크-보라 계열로 그려봤다.

 

레이아웃은 크게 상단바, 사이드바, 본문 영역으로 나뉜다.

 

  • 상단바 왼쪽에는 사이트 이름(미정), 오른쪽에는 탭 메뉴가 있다.
  • 사이드바에는 사용자 프로필과 친구 목록이 나오도록 하여 친구 이름을 클릭했을 때 해당 사용자와 채팅하는 화면으로 넘어가는 것을 생각했다.
  • 상단바 메뉴를 클릭했을 때 클릭한 메뉴에 따라 본문 영역의 내용이 바뀌도록 한다.

 

프로토타입을 바탕으로 친구 검색 화면, 채팅 화면 레이아웃을 구현했다.

 

친구 검색 화면
채팅 화면

우선은 기능 구현을 위해 각 영역만 잡고 디테일한 CSS는 나중에 수정할 것이다.

 

 

2. 친구 검색하기

채팅을 할 친구를 추가하면 사이드바 친구 목록에 추가될 것이고, 그 친구에게 채팅을 보낼 수 있다.

따라서 아이디를 이용해서 다른 사용자를 검색하는 기능을 구현했다.

사용자 정보는 Firestore에 저장되어 있으므로 forEach문으로 조회한다.

 

    const friendId = props.friendId;
    const [isLoaded, setLoaded] = React.useState(false);
    const [friendName, setFriendName] = React.useState('');

    //firebase firestore에서 해당 유저 검색
    const findUser = () => {
        firestore.collection('users').get().then((docs) => {
            docs.forEach((doc) => {
                if (doc.data().id == friendId) {
                    setFriendName(doc.data().name);
                    setLoaded(true);
                }
            });
            setLoaded(true);
        })
    }

 

상위 컴포넌트에서 props로 받아온 아이디를 검색하는 코드이다.

 

 

해당 아이디와 일치하는 사용자가 있으면 화면에 나타난다.

현재는 테스트 편의성을 위해 자신의 아이디도 검색 가능하도록 했다.

 

 

3. 사용자 정보 Redux로 관리하기

로그인 이후부터는 로그인한 사용자 정보가 컴포넌트 곳곳에서 사용될 것인데,

이를 위해서는 전역 상태 관리가 필요하다고 느꼈다.

이것을 위해 Redux(리덕스)를 이용하여 로그인한 사용자 정보를 관리하도록 구조를 변경했다.

 

configStore.js

import { createStore, combineReducers, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import user from './modules/user';
import { createBrowserHistory } from 'history';

export const history = createBrowserHistory();

const middlewares = [thunk];

const enhancer = applyMiddleware(...middlewares);
const rootReducer = combineReducers({user});
const store = createStore(rootReducer, enhancer);

export default store;

 

user.js

import {firestore} from '../../services/firebase';

const user_db = firestore.collection('users');

//Actions
const GET_USER = 'user/GET_USER';
const IS_LOADED = 'user/IS_LOADED';

const initialState = {
    email: '',
    id: '',
    name: '',
    is_loaded: false,
}

//Action Creators
export const loadUser = (email, id, name) => {
    return {type: GET_USER, data: {email:email, id:id, name:name}};
}

export const isLoaded = (loaded) => {
    return {type: IS_LOADED, loaded};
}

//DB에서 사용자 정보 읽어오는 함수
export const getUserFB = (email) => {
    console.log('액션 생성 : DB에서 유저 정보 읽어오기');
    return function (dispatch){
        user_db.doc(email).get().then((info) => {
            const id = info.get('id');
            const name = info.get('name');
            dispatch(loadUser(email, id, name));    //액션 발생시키기
            dispatch(isLoaded(true));
        })
    }
}

//Reducer
export default function reducer(state = initialState, action = {}){
    switch(action.type){
        //do reducer stuff
        case 'user/GET_USER': {
            return {email: action.data.email, id: action.data.id,
                name: action.data.name};
        }
        case 'user/LOADED': {
            return {...state, is_loaded: action.loaded};
        }
        default:
            return state;
    }
}

 

getUserFB가 Firestore에서 사용자 정보를 가져오는 부분이고

loadUser이 사용자 정보 상태를 업데이트 하는 액션을 생성하는 함수이다.

Reducer에서 상태 변경을 반영한다.

 

index.js에 Provider로 store를 주입한다.

ReactDOM.render(
  <Provider store={store}>
      <BrowserRouter>
        <App />
      </BrowserRouter>
  </Provider>,
  document.getElementById('root')
);

 

App.js에서 mapStateToProps와 mapDispatchToProps를 작성해서 상태를 만들고 Redux의 함수를 연결했다.

//스토어가 가진 상태값을 props로 받아오기 위한 함수
const mapStateToProps = (state) => {
  return {
    user_email: state.user.email,
    user_id: state.user.id,
    user_name: state.user.name,
    is_loaded: state.user.is_loaded,
  };
}

//상태 값을 변화시키기 위한 액션 생성 함수를 props로 받아오기 위한 함수
const mapDispatchToProps = (dispatch) => {
  return {
    load: (email) => {
      dispatch(getUserFB(email));
    },
  }
}

 

Profile.js에서 사이드바에 이름과 아이디가 출력되도록 useSelector을 이용했다.

import { useSelector, useDispatch } from 'react-redux';

function Profile(props) {
    const id = useSelector(state => state.user.id);
    const name = useSelector(state => state.user.name);

    return(
        <ProfileConatiner>
            <ProfileImg/>
            <h3>{name}</h3>
            <p>@{id}</p>
        </ProfileConatiner>
    )
};

 

 

직접 코드를 작성해보니 Redux에 대한 개념이 굉장히 헷갈렸지만

다행히도 몇시간 고민하다가 제대로 이해할 수 있었다.

프로젝트 첫번째 날, 회원가입과 로그인 기능을 구현했다.
사용한 기술은 다음과 같다.

  • 회원가입, 로그인 인증 - Firebase Authentication
  • 유효성 검사 - Formik, Yup 라이브러리
  • 사용자 정보 저장 - Firebase Firestore


웹페이지에 처음 접속하면 로그인과 회원가입 버튼을 볼 수 있다.

 

회원가입

회원가입은 이름, 이메일, 아이디, 비밀번호를 입력하여 진행한다.
아직 이메일, 아이디 중복 체크와 비밀번호 확인 기능은 구현하지 않았다.
Firebase Authentication을 이용해서 인증을 하고, Firestore에 사용자 정보를 저장하는걸 구현하는게 우선이기 때문!

그래도 유효성 검사는 미리 해두는게 좋을 것 같아서
리액트에서의 유효성 검사에 대해 찾아보다가 Formik 라이브러리와 Yup 라이브러리를 이용한 사례를 찾았다.

라이브러리를 사용하지 않고 유효성 검사를 구현하려면 코드가 굉장히 길어지고
각 폼의 양식마다 입력한 값을 관리하기가 쉽지 않다고 한다.
이것을 편하게 할 수 있도록 Formik 라이브러리가 제공되며
Yup을 이용해 각 필드의 유효성 검사 스키마를 쉽게 작성할 수 있었다.

내가 참고한 포스팅 링크 : https://velog.io/@roh160308/%EB%A6%AC%EC%95%A1%ED%8A%B8React-Formik-Yup

이런 식으로 유효성 검사에 어긋나는 값을 입력한 경우, input 아래에 자동으로 메세지가 출력된다.
이름 유효성 검사에 대한 코드만 간단히 작성하자면 다음과 같다.

<Formik initialValues={{ name: '', email: '', id:'', password: ''}}
	validationSchema={
    	Yup.object({ name: Yup.string()
        .max(10, '이름은 10글자 이하로 작성해주세요.')
        .required('이름을 입력해주세요.'), })}
        onSubmit = {(values, { setSubmitting }) => {
        	//모든 유효성 검사를 통과했을 때
        }}>
        {formik => (
        	<form onSubmit={formik.handleSubmit}>
            	<Input id="name" type="text" placeholder="이름"
                	{...formik.getFieldProps('name')} />
                    {formik.touched.name && formik.errors.name ? ( <div>{formik.errors.name}</div> ) : null}
                <SignUpBtn type="submit">가입하기</SignUpBtn>
            </form>
        )}
 </Formik>

Formik 태그 안에 폼 양식과 유효성 검사 스키마를 작성했다.
Yup 라이브러리에서 max로 최대 글자수 제한을, required로 작성 여부 확인을 할 수 있다.
아주 간단하게 코드 작성이 가능해서 유용했다!

로그인

회원가입을 완료하면 Firestore에 사용자 정보를 등록하고 나서
로그인 화면으로 이동하게 되고 이메일과 비밀번호를 입력하여 로그인할 수 있다.

사용자 계정 정보는 Firebase Authentication을 이용했고 로그인을 할 때 현재 세션에서 인증 상태를 유지하도록 했다.
참고한 공식 문서 : https://firebase.google.com/docs/auth/web/auth-state-persistence?hl=ko

로그인에 성공하면 alert가 뜨고 메인 화면으로 이동한다.

로그인 성공 후 메인 화면

로그인 상태를 현재 세션동안 유지되도록 하는 것은 어렵지 않았는데
App.js에서 로그인 상태를 파악하기 위해 이것저것 시도하느라 시간이 꽤 걸렸다.
해결한 방법은 Firebase auth의 onAuthStateChanged 메소드를 이용하는 것이었다.

 componentDidMount() {
 	auth().onAuthStateChanged((user) => {
    	if(user) { //로그인한 상태일 때
        	console.log('로그인한 이메일 : ' + user.email);
            this.setState({email: user.email});
        } else { //로그인 안한 상태일 때
        	console.log('로그인 안된 상태');
        }
    });
 }


리액트 라이프 사이클 함수 중 첫번째 렌더링을 마친 후에 실행되는 componentDidMount에서
onAuthStateChanged를 이용하여 로그인 여부를 파악해서 로그인이 되어있으면 state를 변경하여 리렌더링한다.
관련 내용을 찾다가 react-redux-firebase라는 패키지가 있는 것을 알게 되었는데
추후에 redux도 사용할 예정이라 해당 패키지에 대해서 좀 더 조사해봐야겠다.



지난 달에 리액트 강의를 듣긴 했으나 직접 코드를 작성하는 것은 처음이라 기억이 가물가물한 부분도 많았고
새로운 라이브러리를 사용하니까 사용법을 익히는데 시간도 좀 걸렸다.
하지만 역시 눈으로 보는 것에 비해 실제로 코드를 작성하는 것이 학습 효과가 더 뛰어난 것 같다.
프로젝트 첫날이지만 많은 것을 복습하고 배울 수 있었다!👍

방학동안 웹 개발 공부를 많이 못한거 같아서 강의로 들었던 리액트를 기반으로 한 프로젝트를 진행해보려고 한다.

나는 메신저로 카카오톡과 슬랙을 이용하고 있는데 이런 메신저를 웹으로 구현하면 어떨까 싶어서

프로젝트 주제를 웹 메신저로 정했다.

 

웹 메신저를 구현할 때 제공할 기능과 사용할 기술을 적어보았다.

 

1. 회원가입, 로그인, 로그아웃

  • Firebase Authentication의 이메일 인증 방법 사용
  • Firebase Firestore에 사용자 정보 저장

2. 프로필 설정

  • 이름은 Firestore에 수정 반영, 프로필 이미지는 Firebase Storage에 저장할 것

3. 친구 검색 및 추가

  • 아이디로 친구 검색, 추가
  • 추가한 친구 목록 조회

4. 채팅방 입장 및 채팅 전송

  • 친구 목록에서 채팅을 시작할 친구 선택
  • 개설된 채팅방은 메인 화면에 출력
  • 채팅 내역은 Firebase Realtime Database에 저장하여 실시간 동기화가 가능하도록 함

 

기본적으로 제공할 기능은 위와 같고 만약 위의 기능을 모두 구현했다면

단체 톡방, 채팅 내역 개별 삭제, 커뮤니티 기능 등 확장할 수 있는 기능이 굉장히 많으므로

다양한 기능을 구현하면서 실력을 쌓고 여러 기술들을 활용해볼 수 있을 것이다.

 

우선은 앱 개발 프로젝트에서 사용해본 경험이 있기 때문에 익숙한 Firebase를 주로 사용할 것이고

리액트의 상태 관리와 비동기 처리를 다루는 실력을 많이 키울 수 있을 것으로 기대한다.

 

이번 2학기는 취준 겸 프로젝트를 하면서 공부하는걸로~

2021년 1학기 캡스톤디자인 작품으로 코딩 멘토-멘티 스터디 진행이 가능한 웹 사이트를 제작했다.

사이트 이름은 모두의 코딩교실을 줄인 모코(MOCO) 이다.

Eclipse에서 Spring Boot 프로젝트로 개발했고 JSP, CSS, JS를 이용해서 전체 페이지를 구성하였다.

 

나는 전반적인 웹 사이트 레이아웃을 구현하고

로그인, 글 작성/수정/삭제, 그룹 관리, 알림 등의 기능을 구현했다.

 

부트스트랩 프레임워크를 이용해서 반응형 웹을 손쉽게 구현할 수 있었는데

부트스트랩과 직접 만든 CSS간의 충돌이 발생해서 개발 과정에서 혼란을 겪기도 했다.

 

DB의 경우 MySQL과 연동하여 MVC 패턴의 MyBatis 방식으로 이용했다.

 

이렇게 5인 이상의 팀 단위 프로젝트를 진행한 것도,

GitHub를 이용하여 협업을 한 것도 처음이었던 프로젝트라 의미가 있었다.

 

GitHub 👉 https://github.com/nbalance97/mentomenti

 

nbalance97/mentomenti

2021-1 캡스톤디자인. Contribute to nbalance97/mentomenti development by creating an account on GitHub.

github.com

 

메인 화면
로그인 화면
개설된 그룹 목록
가입한 그룹 목록
그룹 개별 페이지
알림 목록 페이지
회원 정보 페이지
수업 - 화면 공유
수업 - 웹 컴파일러

 

+ Recent posts