와플스튜디오 20.5기 Rookies Repository

  • By Wafflestudio
  • Last update: Nov 24, 2022
  • Comments: 8

와플스튜디오 2022 세미나 레포지토리



wafflestudio_logo

2022 세미나는 다음 학기부터 개발 프로젝트에 참여할 수 있는 20.5기 Programmers의 양성기존 부원의 실력 향상 을 목표로 합니다.

  • React
    • JavaScript
  • Django
    • Python
  • Kotlin Spring MVC + JPA
    • Kotlin
  • Android
    • Kotlin
  • iOS
    • Swift



OT


시간 장소 자료 참여 링크 영상
2022년 8월 21일 ZOOM PDF PPT ZOOM

세미나


세미나 시간

세미나 요일 시간
Django 화요일 오후 7시 30분
Spring 화요일 오후 7시 30분
IOS 화요일 오후 7시 30분
Android 목요일 오후 8시
React 토요일 오후 3시

세미나 진행상황

  • 각 세미나의 일정과 과제, 이전 자료 등을 확인할 수 있습니다
  • 세미나 별 첫 번째 세미나 일정이 업로드 되었습니다 (08/21)

React
회차 시간 장소 자료 과제 참여 링크 영상
seminar0 8월 27일 (토) 오후 3시 30분 스프링 라운지 2층 PPT 과제 A 과제 B 영상
seminar1 9월 3일 (일) 오후 3시 비대면 PPT 과제 영상
seminar2
seminar3
seminar4
seminar5


Django

수업 일정 및 자료



Kotlin Spring MVC + JPA

수업 일정 및 자료



Android
회차 시간 장소 자료 과제 참여 링크 영상
seminar0 8월 25일 (목) 오후 8시 서울대입구 히든스페이스 3번방 자료
seminar1
seminar2
seminar3
seminar4
seminar5


iOS
회차 시간 장소 자료 과제 참여 링크 영상
seminar0 9월 6일 (화) 오후 7시 30분 서울대입구 히든스페이스 3번방 자료 과제 줌 링크 영상
seminar1 9월 13일 (화) 오후 7시 30분 서울대입구 히든스페이스 3번방 자료 과제 줌 링크 영상
seminar2 9월 20일 (화) 오후 7시 30분 서울대입구 히든스페이스 3번방 자료 과제 줌 링크 영상
seminar3 10월 4일 (화) 오후 7시 30분 서울대입구 토즈 모임센터 D룸 자료 과제 줌 링크 영상
seminar4 11월 1일 (화) 오후 7시 30분 서울대입구 히든스페이스 3번방 줌 링크
seminar5 11월 15일 (화) 오후 7시 30분 서울대입구 히든스페이스 3번방 줌 링크


Github

https://github.com/wafflestudio/seminar-2022

Comments(8)

  • 1

    Android Codelab 링크 관련 질문드립니다.

    제공해주신 해당 Android Codelab 링크 https://developer.android.com/courses/kotlin-android-fundamentals/overview 로 접속하니 "Caution: This course is no longer maintained. Please use Android Basics in Kotlin or Android Basics with Compose instead." 라는 문구가 나타납니다. 혹시 명시된 대로 Android Basics in Kotlin 에 연결된 https://developer.android.com/courses/android-basics-kotlin/course 링크의 course로 과제 진행해도 문제 없을까요?

    감사합니다 :)

  • 2

    세미나 참여 기준과 관련하여 질문 드립니다.

    안녕하세요. 와플스튜디오 20.5기 루키 유창민이라고 합니다. 세미나 참여 기준과 관련하여 질문할 사항이 두 가지 있어 질문 드립니다.

    1. 세미나에 비대면으로 참여하는 경우에도 줌을 통해 해당 세미나 시간 동안 접속한다면 출석으로 인정받을 수 있나요?
    2. 만약 개인사정으로 인해 실시간 세미나에 참석하기 어려운 경우, 출석 인정을 위한 대체 방법이 있나요?

    감사합니다!

  • 3

    React 과제 2 공통 피드백

    2022 React Seminar 과제 2 공통 피드백

    DOM 접근

    특별한 몇몇 경우가 아닌 이상 getElementById나 기타 방식으로 DOM 객체를 직접 다뤄야 할 일은 별로 없습니다. 99%의 경우 useState로 커버되고, 0.99%는 useRef로 DOM을 얻어올 수 있습니다.

    a 요소 (또는 Link 컴포넌트)

    어떤 요소를 클릭하면 페이지가 이동하는 경우, aLink로 구현해주시는 게 접근성 면에서 좋습니다. 브라우저가 링크로 인식한다는 점에서 스크린 리더나 브라우저 확장기능과 잘 결합할 수 있고, 커서를 올렸을 때 이동할 링크가 나타난다는 특징도 있습니다.

    Controlled input

    input 요소의 값이 리액트에서 사용되는 경우 onChangevalue props를 둘 다 사용하여 관리해야 합니다. 예기치 않게 input의 값과 react의 값이 달라지는 경우를 막을 수 있습니다.

    사이드이펙트는 useEffect 안에서

    함수 컴포넌트 안에는 훅 함수(useState, useEffect 등)를 제외하고는 순수한 동작만 있어야 합니다. 즉, setTimeout으로 타이머를 설정하거나 상태를 변경하는 등 순수하지 않은 코드는 렌더링 중에 실행되어서는 안 됩니다. 그러한 동작은 모두 useEffect 내에서 실행하세요.

    function Example() {
      // don't:
      setTimeout(() => {
        alert("100ms 후의 미래");
      }, 100);
      
      // do:
      useEffect(() => {
        setTimeout(() => {
          alert("100ms 후의 미래");
        }, 100);
      }, []);
      
      /* ... */
      return null;
    }
    

    enum

    • 0, 1과 같이 단순한 숫자에 특별한 의미를 부여하여 사용하는 경우가 있습니다. 그럴 때에는 각 상수를 const로 이름을 붙여 사용하는 것이 좋습니다.
    // 상황 자체는 좋은 예시가 아닙니다. 상수를 선언하여 사용하는 부분에 주목해주세요.
    const CANCEL = 0, MODIFY = 1;
    setButtonBehavior(CANCEL);
    

    let

    이젠 괜히 쓰시는 분 없으리라 생각합니다. 값이 변하지 않는 변수는 언제나 const로 선언해야 합니다.

    이름

    컴포넌트는 반드시 PascalCase로 써야 하고, 그 외의 javascript에서 쓰는 이름은 99% camelCase로 씁니다. 파일명은 컴포넌트의 경우 export하는 컴포넌트의 이름과 똑같이 쓰고 그 외에는 적당히 camelCase로 지어주시면 됩니다. 아 컨텍스트도 대문자로 시작하게 써주세요.

    레이아웃 오류

    보통 height 속성을 직접 지정하면 화면 높이가 약간 안 맞아서 스크롤이 생기곤 합니다. flexgrid로 해결하는 게 개인적으로 가장 깔끔하다고 생각하고, 크기를 꼭 직접 지정하고 싶다면 box-sizing: border-box를 사용해보시면 도움이 될 수 있습니다. 아직 레이아웃 잡는 게 힘드시다면 세미나2.5 참고해서 flex랑 grid 써보세요.

    key prop

    key prop의 기능은 리스트 안의 컴포넌트들을 구분하는 기능 그거 하나뿐입니다. 리스트 밖에서는 사용하면 안 된다는 건 아니지만 동작 면에서도 가독성 면에서도 별다른 기능이 없고 코드 리뷰에 한 줄 추가되는 기능 뿐입니다.

    id prop

    HTML에서 id는 주로 document.getElementById할 때 많이 쓰게 되는데 리액트에서는 거의 쓸 일이 없습니다. 괜히 id 충돌날 건덕지만 만드는 일이니까 스타일 지정하고 싶다면 className 쓰는 게 좋을 것 같습니다. 저번엔 keyid에 같은 값을 내려주는 코드도 많이 봤는데, 둘은 기능이 전혀 다르니 잘 구분해서 사용하시기 바랍니다.

    dependency array

    useEffect, useMemo 등등은 두번째 인자로 dependency array를 받습니다. 이걸 그냥 비워놓으신 분들 꽤 있던데 함수 안에서 사용하는 이름은 import로 가져온 거 빼고 전부 넣어준다고 생각하시면 됩니다. 특정 값이 변할 때만 무언가 동작하도록 일부러 빼는 경우도 있을텐데, 그러면 99% 오류 납니다. 그런 건 useEffect 안에서 if 문으로 처리하시면 되구요 아무튼 핵심은 dependency array 잘 넣으시라는 겁니다.

    Context를 통한 관심사 분리

    React Context의 주요한 기능은 props drilling을 막는 것이지만, state 등을 분리하는 김에 관련된 기능들도 여러 관심사에 따라 분리하고 추상화할 수 있습니다. 예를 들면 메뉴 목록을 state로 만들어 setState와 함께 내려주는 방법도 있지만:

    function MenuDataProvider({children}) {
      const [menus, setMenus] = useState([]);
      const [maxId, setMaxId] = useState(4);
      return <MenuDataContext.Provider value={{menus, setMenus, maxId, setMaxId}}>
        {children}
      </MenuDataContext.Provider>;
    }
    

    한편으로는 관련된 기능들을 함수로 만들어 내려주는 방법도 있습니다. 이 경우에는 maxId와 같이 사용 방식을 제한된 변수들을 다른 컴포넌트들에게서 숨겨 추상화할 수 있습니다.

    function MenuDataProvider({children}) {
      const [menus, setMenus] = useState([]);
      const [maxId, setMaxId] = useState(4);
      const addMenu = useCallback(() => { /* ... */ });
      return <MenuDataContext.Provider value={{menus, addMenu, /* ... */}}>
        {children}
      </MenuDataContext.Provider>;
    }
    
  • 4

    React 세미나2 url 변경 시 context 값이 초기화되는 현상에 관한 질문

    안녕하세요. 세미나2 과제 관련해서 구글 검색을 해도 방법을 찾지 못해 질문드립니다.

    URL을 변경하면, 기존의 메뉴와 로그인 context가 초기화되는 문제입니다. 예를 들면 아래와 같습니다.

    • 로그인한 상태에서 URL을 통해 이동하고자 menus/4를 입력하면 로그인 상태가 해제됩니다.
    • 메뉴를 추가한 뒤, 추가된 메뉴를 보고자 URL에 stores/1를 입력하면 기본 메뉴만 남아있습니다.

    헤더를 비롯한 각 컴포넌트 내의 Link 를 이용하여 이동할 때는 이러한 문제가 발생하지 않습니다.

    현재 제 App 컴포넌트는 대략 아래와 같습니다. (과제 수행과 관련하여 문제되는 부분이 있다면 수정하겠습니다)

    ... // data.json을 data로 import한 상태
    function App() {
      const [menuItems, setMenuItems] = useState(data);
      const [loginObj, setLoginObj] = useState({
        isLogin: false,
        id: "",
        password: "",
        });
      return (
          <div>
            <SessionContext.Provider value={{ loginObj, setLoginObj }}> // SessionContext.js 파일을 import한 상태
              <MenuDataContext.Provider value={{ menuItems, setMenuItems }}> // MenuDataContext.js 파일을 import한 상태
                <BrowserRouter>
                  <Routes> 
                      ... // Route 들
                  </Routes>
                </BrowserRouter>
              </MenuDataContext.Provider>
            </SessionContext.Provider>
          </div>
      });
    

    문제가 발생한 이유를 파악하기 위해 이 App 컴포넌트의 return 문 위에 console.log를 입력해보았는데, URL을 바꿔 입력할 때마다 콘솔에 찍혔습니다. 이를 통해 App의 useState를 이용한 부분 역시 URL이 바뀌어 입력될 때마다 새로 실행되고, 이 때문에 menuItemsloginObj역시 초기화되는 것이라고 생각하였습니다. 그러나 이후에 어떻게 이 문제를 해결해야할지에 대한 방법을 찾지 못하고 있습니다. 혹시 이와 관련하여 도움 주신다면 정말 감사하겠습니다!

  • 5

    React 세미나2 로그인 안된 상태에서는 접근금지

    
    import './New.css';
    import {useCounterContext} from "./CounterContext";
    import Header from "./Header";
    import {useNavigate} from "react-router-dom";
    import React, {useState} from "react";
    
    function New() {
        const {addMenu, menus, loginStatus} = useCounterContext();
        const [enteredNum, setEnterdNum] = useState("");
        const changeEnteredNum = (e) => {
    
            const value: string = e.target.value;
            const removedCharacterValue = value.replace(/[^0-9]/g, '');
    
            const removedCommaValue: number = Number(removedCharacterValue.replaceAll(",", ""));
            setEnterdNum(removedCommaValue.toLocaleString());
        };
        const navigate = useNavigate();
        const Goout = () => {
            navigate("/stores/1");
        };
    
        const addbutton = () => {
            const inputName = document.getElementById('name').value;
            const inputType = document.getElementById('typeselect').value;
            const inputImage = document.getElementById('image').value;
            const inputDes = document.getElementById('description').value;
            if (inputName === "" || enteredNum === ""|| inputType=== "") {
                alert("빼먹은 부분이 없느지 확인해주세요");
            } else {
                let isexist = menus.some((menu)=>menu.name===inputName);
                if (isexist === false) {
                    if (inputName.length <= 20) {
                        if (Number(enteredNum.replaceAll(",", ""))>=100 && Number(enteredNum.replaceAll(",", ""))<=1000000) {
                            addMenu(inputName, enteredNum, inputImage, inputType, inputDes);
                            Goout();
                        }
                        else {
                            alert("가격이 범위에 맞지 않습니다. (100<price<1000000)");
                        }
                    }
                    else {
                        alert("이름이 너무 깁니다.");
                    }
                }
                else {
                    alert("동일한 이름의 제품이 이미 존재합니다.");
                }
            }
        };
    
        if (loginStatus === true)
        {
            return 1;
        }
        else
        {
            alert("GoOut");
            navigate(-1);
            return 0;
    
        }
        {/*return (
                <>
                    <Header/>
                    <section className="Newsection">
                        <div className="title">새 메뉴 추가</div>
                        <div className="nameadd-str">이름</div>
                        <input type="text" placeholder="맛있는와플" id="name" className="nameadd-box"/>
                        <div className="typeadd-str">종류</div>
                        <select id="typeselect" className="typeadd-box">
                            <option value='' selected>상품의 종류를 선택하세요</option>
                            <option value='coffee'>커피</option>
                            <option value='waffle'>와플</option>
                            <option value='beverage'>음료</option>
                        </select>
                        <div className="priceadd-str">가격</div>
                        <input type="text" value={enteredNum} onChange={changeEnteredNum} placeholder="5,000"
                               className="priceadd-box"/>
                        <span className="unit">원</span>
                        <div className="imageadd-str">상품 이미지</div>
                        <input type="text" placeholder="이미지 주소를 입력해주세요" id="image" className="imageadd-box"/>
                        <div className="desadd-str">설명</div>
                        <textarea placeholder="상품에 대한 자세한 설명을 입력해주세요" id="description" className="desadd-box"></textarea>
    
                    </section>
                    <button className="add" onClick={addbutton}>
                        추가
                    </button>
                    <button className="close" onClick={Goout}>
                        취소
                    </button>
                </>
    
            );*/}
    
    
    }
    
    export default New;
    
    

    menus/new에 loginstatus가 false면 접근하지 못하게 하려고 합니다. 일단 new 다 만들어두었고 그 기능만 추가하면 됩니다. 하지만 테스트하는 과정에서 alert는 두번 뜨고 navigate(-1)은 작동을 안하는 것을 확인했습니다. 어떻게 고쳐야하나요?

  • 6

    ArgumentResolver에서 request에 지정한 attribute 꺼내오기

    안녕하세요, 2차시 세션에서 설명이 제대로 이루어지지 않은 부분이 있었는데요!

    저희가 package javax.servlet.http 안에 있는 HttpServletRequest를 사용해서 setAttribute 해주기 때문에, 어트리뷰트를 가져올 때도 이 객체로부터 정보를 얻어와야 합니다.

    request.getAttribute() 메소드를 사용할 때, 스프링에 내장된 NativeWebRequest 타입의 getAttribute 메소드는 시그니처로 name뿐만 아니라 scope를 받습니다. 스코프는 세션 레벨에서의 다양한 정보 저장을 위한 기능인데요,

    아래처럼 NativeWebRequest 타입의 요청을 ServletWebRequest라는 객체로 감싸주면 (어댑터 클래스입니다), 그 객체 내부에 있는 request 프로퍼티를 통해 인터셉터에서 가로챘던 HttpServletRequest 에 접근할 수 있습니다.

    
        override fun resolveArgument(
            parameter: MethodParameter,
            mavContainer: ModelAndViewContainer?,
            webRequest: NativeWebRequest,
            binderFactory: WebDataBinderFactory?
        ): Any? {
            parameter.hasMethodAnnotation(UserContext::class.java)
            val password = (webRequest as ServletWebRequest).request.getAttribute("pwd")
            return password
        }
    
    

    image

  • 7

    Kotlin으로 풀어보는 20.5기 코테 3번 문항에 대한 질문

    @Jhvictor4 님의 예시 답안 링크

    class StudentTable(
        private val students: LinkedList<String>                     // (1)
    ) {
        private var cursor = 0                                       // (2)
        private val cache = mutableListOf<Cached>()                  // (3)
    
        private data class Cached(
            val originalIndex: Int,
            val studentName: String,
        )
    
        ...
    
        private fun moveDown(count: Int) {
            val resultLoc = cursor + count                           // (2)
            if (resultLoc >= students.size) {                        // (1)
                println("Error 100")
                return
            }
    
            cursor = resultLoc
        }
    
        ...
    
        private fun delete() {
            val removed = this.students.removeAt(cursor)              // (1)
            this.cache.add(Cached(cursor, removed))                   // (3)
    
            // now current Index has moved to it's next one
            // reallocate current index if no next exists
            if (cursor == students.size) {                            // (1)
                cursor -= 1
            }
        }
    
        ...
    
    }
    

    질문

    • (1) : LinkedList 타입의 students 변수 (생성자)

      • 제가 처음 코틀린을 접할 때 분명히 students의 element가 바뀔 것이라 생각하여 변수 정의 시 var를 예측하였으나 예시 답안은 val이었습니다. @dntjr8096 님과 구글의 검색 결과, LinkedList의 주소 자체는 바뀌지 않기 때문에 val을 써도 된다는 의견을 얻었습니다. 이때 (불변하는) 주소란, LinkedList 내부 element 각각의 주소가 아니라 LinkedList 자체의 주소를 지칭한다고 봐도 될까요?
      • moveDown과 delete 함수에서 쓰이는 students 변수 쓰임이 다릅니다. moveDown에서는 students.size를, delete에서는 this.studetns.removeAt(cursor)를 사용하여, this의 유무가 차이가 납니다. 일반적으로 생성자에 정의되었기 때문에 this를 붙임으로써 StudentTable 클래스 자체를 지칭한다고 하지만 굳이 this를 붙이지 않아도 상관 없는지, 붙이면 어떤 것이 좋은지 궁금합니다.
    • (2) : Int 타입의 cursor 변수 (클래스 내부 변수)

      • cursor는 명확히 가변적인 Int이므로 students나 cache에서 들었던 "List의 element는 변할텐데 왜 val을 사용했지" 라는 질문은 들지 않았습니다. 다만 cursor와 더불에 StudentClass 내부에 나란히 정의된 cache 변수와 비교했을 때 cursor는 this가 붙지 않았다는 점에서 비교대상으로 삼았습니다.
    • (3) : mutableListOf< Cached > 타입의 cache 변수 (클래스 내부 변수)

      • (2)의 맥락에 따라 this.cache.add(Cached(cursor, removed))에서는 왜 this가 붙어서 굳이 StudentClass를 지칭하는지 궁금합니다. (cursor는 붙지 않았다는 것과 비교하여)
      • (1)에서의 질문과 마찬가지로 왜 val이 붙었는지 궁금합니다.
  • 8

    docker mysql 연결 문제 질문

    Docker로 띄운 MySQL을 Datagrip으로 연결하다가 발생한 문제를 해결했는데 왜 됐는지.. 모르겠어서 질문을 올립니다.

    우선은 사용하는 운영체제는 윈도우입니다.

    Docker로 MySQL을 실행시킨뒤 Docker의 Mysql cli를 사용해서 mysql -u root -p 와 비밀번호를 입력해서 들어가면 잘 들어가지는 상황이었는데 Datagrip을 통해 올바른 user와 password를 쳐도 비밀번호가 틀렸다는 access denied 오류가 계속 발생했습니다.

    • 그냥 컴퓨터에도 mysql이 깔려있는 상황이었습니다.

    이것저것 해보다가 127.0.0.1 3306번 포트의 PID를 taskkill 했는데 문제가 해결이 되어서.. 이게 왜 된건지 혹시 아시는 분 도움주실 수 있을까요..?

    정리하면 아래 두가지가 궁금합니다!

    1. Docker cli로 접속했을 때는 왜 정상적으로 db에 접속할 수 있었고 datagrip을 사용해 접속하려하면 안됐는지
    2. 3306포트의 PID를 taskkill 했더니 문제가 왜 해결된건지..?

    Docker나 포트같은 개념에 대해 잘알지못해서,, 혹시 답변을 위해 더 필요한 정보가 있다면 추가해보도록 하겠습니다!