글예 6강

  • 르네상스 이후 모던이 어떻게 형성되었는지 설명할 수 있다.
  • 인상주의의 개념과 특징, 대표작품에 대해 설명할 수 있다.
  • 세잔과 메를로 퐁티의 특징과 작품에 대해 설명할 수 있다.


  1. 모던의 형성
    1. 19세기 말 유럽.
      1. 급격한 기술 진보와 새로운 사물의 등장.
        1. 축음기, 사진, 자동차, 전화 , 엑스레이 , 비행기
      2. 도시의삶, 자본주의화된 삶, 익명적인 삶
      3. 시간과 공간의 변화.
      4. 만국박랍회 1789년 (프랑스혁명) 이후 100년 자축행사라고해도 과언이아님. 
        1. 동양 ->오사카 만국박람회. 
      5. 개인. 익명적인 삶일수록 전체에 흡수되기 쉽다.
    2. 근대 미술의 흐름
      1. 사진의발명 >> 그림의 재현기능을 무화시킴
      2. 새로움에대한동경 >> 전통에 대한 반역, 규범의 파괴 -> 자신만의 스타일을 만들어봄. 바로 새로움의 미학.
      3. 새로움의 미학 >> 예술은 형, 색, 공간을 재현하는 것이 아니라 새로움을 보여주는 것이어야함.
      4.  '새로운 과격함, 변화를 이야기하는 예술의 등장. ' > 20세기 아방가르드에서 절정.
    3. 예술가들의 위상변화
      1. 귀족 및 부르주아 세력과 분리 심화
        1. 근대 부르주아의 화가 후원. 그러므로 후원받지않는 작가들이 더 자유롭게 그릴수잇다.
      2. 세속적 화가들과 이단자그룹으로 양분
        1. 이단자그룹 : 예술혼, 창작의 욕구를 위해 배고픈 자가 되기를 자처함.
      3. 권위로부터 자유로워진 예술은 사회 비판적 역할을 담당하기 시작.
      4. "무관심성으로서의 예술"-칸트
  2. 인상주의
    1. 인상주의
      1. 19세기 후반 프랑스를 중심으로 일어난 미술 사조.
      2. 1860년대 파리의 미술가(마네 모네 드가 바지오.. =>> 그룹지어다님. 이단자그룹이었듬 )들이 주도함.
      3. 예술 아카데미의 훈력과 살롱전 데뷔의 전통에 반기.
        1. 스스로 해석한 현대성의 원칙에 따라 그림.
        2. 살롱/ 아카데미가 원하는 화풍(신고전주의 , 아름다운 현상)이 있었는데 거기에 반기를 들고 자신이 원하는것을 그려내겠다 주장함.>> 인상주의 라고 이름을 붙이게됨.
    2. 모던예술 (인상주의의 특징)
      1. 그림을 그린다는것 -> 아주 정확하게 원근법을 쓴다던지, 파레트에 물감을 섞어서 정확한 색을 표현한다던지. 시간을 들여 고도의 완성도를 요하는 그런 그림이었음. 
      2. 실내에서 재현원칙에 따른 원근법, 색체학에 입각한 그림 반대
      3. 현장에서 눈에 보이는 빛의 운동감과 속도감을 그리려 함
        1. 이게 현실이니까!
      4. 빛에 따라 변화는 색의 가변성을 포착하려함
      5. 미완성, 불완전성, 변화 등의 특징을 가짐
      6. 공간을 없애고 가변적 이미지를 남김
      7. 소재가 파격적, 특히 자연광을 중요시 하기 때문에 풍경화 많음.
        1. 매춘부, 벗은 여성. 소재도 파격적이지만 표현해내는 기법또한 파격적! 원근법보단 자연광 사용한다던지..
      8. 공간적 깊이를 상실하여 평면성 강함.
      9. 감정에 상관적인 색감, 질 강조
      10. 속도감의 강조는 점묘법으로 이어짐. 빨.노 찍어놓으면 멀리서보면 주황색으로 표현된다.
      11. 완벽하게 대상에 접근하기보다는 -> 루헨성당 1, 2 3.. 이런식으로.시간에 따라 빛에따라 계속 변하니까.
    3. 마네 ( Manet )
      1. 인상주의의 아버지.
      2. 인상주의 혹평많았고, 상처도 많고..  
      3. 올림피아 -> 매춘부. 당당하게 우리를 쳐다보고있다 악마의 상징인 검은고양이. 
        1. 원근법 찾기힘들고 평면적.
        2. 있는 그대로 그려낸 그림. 
        3. 원근법을 거부했고. 자연광에서 그리는 것처럼, 색채의 빛의 효과. -> 신체를 묘사할 땐 평면을 느끼게함
      4. 풀밭위의 점심
        1. 벗고있고, 아주 담담하게 쳐다보고있다.
        2. 붓질의 터치감이 생생하게 살아있다. 
        3. 인상주의의 혁명적 작품
      5. 피리부는 소년 / 모리조 (인물상)
        1. 화려한 색감을 자랑하는 작품
    4. 모네 ( Monet )
      1. 물을 좋아함.물위의 배. 물위의 수련.
    5. 드가 ( Degars )
      1. 사후에 인정받음. 독특한 특징이있다.
      2. 바라보는 시선자체가 모던의 시선을 가지고있음.
      3. 즉 카메라의 시선을 가짐.
  3. 세잔과 메를로 퐁티
    1. 세잔 (후기 인상 주의)
      1. 인상주의적 특징을 아주많이 가지고있으면서도 인상주의를 넘어선 자신만의 탐구와 연구가 작품에 반영돼있음.
      2. 20세기 근대 회화의 아버지
      3. 입체주의와 대상의 객관적 진실표현의 뿌리 제시
      4. 26세, 28세에 살롱에 응모했으나 낙방.
      5. 35세 인상파전 출품으로 데뷔
      6. 엑상 프로방스에서 색과 빛에 집중과 사물성을 동시에 표현하기 위한 연구를 엑상 프로방스에서 지속
      7. 50세에 첫 개인전, 극찬을 받으며 성공.
    2. 세잔의 작품. 
      1. 아주화려하지도않고. 산의 모습이 인위적으로 보이기도 함. 
      2. 인상들을 그대로 화폭에 담으면서 자신만의 독자적 질서를 부여하고자 했던 의미에서 -> 예술적감성 + 과학적 시선이 동시에 절묘하게 행복하게 만나고있다라고 얘기하기도함.
      3. 세잔의 사과 : 
        1. 저 사과는 보기에 먹음직스러워보이진 않지만, 나에게 말을 건넨다..
    3. 메를로 퐁티(Merleau - Ponty)
      1. 설명
        1. 1908년 프랑스 로슈포르 출생, 
        2. 소르본 대학, 콜라주 드 프랑스 교수
        3. 사르트르의 현대지 창간에 협력
        4. 훗설의 현상학을 발전시켜 행동의 구조와 지각세계의 연구를 통해 관념론과 실재론의 전제 모두 비판
        5. 세잔에 대한 회화론을 남김.
      2. 메를로 폰티의이론
        1. 원초적 지각
          1. 본다는 것은 망막의 현상이 아니라, 인간이 자신을 외부세계와 물리적, 정신적으로 관련 맺는 복합적 과정지각
        2. 신체적 코기토
          1. 코기토 : 인간의 사유하는 능력. 퐁티는 지각하는 주체는 사유가아니라 '신체'라고 함. 
          2. 지각의 주체는 '사유'가 아니라 혼탁한 '신체'임 
          3. 지각하는 최초의 순간 사물의 모습은 혼란스럽게 나타나며, 논리적 반성을 통해 정돈하는 것.
          4. 퐁티는 데카르트가 나눈 신체와 정신을 '살' 속에 녹여 인상주의와 고전주의의 대립을 극복함.
          5. 무언가를 만지는것을 그 사물과 나와의 교집합이 있다는것. 관계를 맺고 소통을 하고있다는것.
  4.  href="/lms/css/framework/ui/themes/dunet/jquery-ui-1.8.custom.css?v=1.1">
    근대가 보여주는 감각적 표현
    주제 지금 여러분이 미술관 학예관을 위한 최종 인터뷰 중이라고 가정해보겠습니다. 심사관은 나에게 메를로 퐁티는 세잔의 회화가 리얼리티에 이르는 수단을 버리고 리얼리티를 추구하고자 했다고 했는데 그것이 어떤 의미인지 본인이 이해한대로 설명해달라고 요구하였습니다. 이 심사관에 말에 대한 여러분의 의견을 입력해보세요.
    No 작성자 의견 작성일
    1 0010201010232 리얼리티로 대변되는 인상주의의 특징에서 보면 주변의 속도감, 빛의 변화 등을 회화에 녹이기 위해 원근감을 택하지 않고 있습니다.
    그렇기 때문에 사물이나 대상의 형태가 명확하지 않고 흐물흐물하게 표현되는데,
    세잔의 경우는 이러한 인상파가 추구하는 빛과 색에는 동의하고 이를 회화에 옮기지만 결과적으로 나오는 대상의 흐릿함은 수용할 수 없었습니다.
    좀 더 현실적인 표현을 하기 위해서는 사물이 흐릿해야 하지만, 대상의 명확함에서 주는 좀 더 리얼리티한 면을 추구한 것입니다.
    대상의 흐릿함을 버리고 사물의 모습을 감각으로 여러 장면에서 포착하고 포착한 사물을 하나의 물체로 종합하여 그려냄으로써 리얼리티를 추구했다고 봅니다.
    2013.09.18 12:15
    2 00032013405001 메를로 퐁티는 화가가 "대부분의 사람들이 세계에 묻혀 보지 못하는 광경을 포착하여 보여주는 사람"이라고 말했다. 이 말을 다시 짚어보면 세잔의 회화는 일반적인 사람이 보는 시선에서 벗어나 그들이 보지 못한 현실을 포착하여 보여주는 것이라 볼 수 있다. 일반적인 사람들이 보지 못한 현실은 허구가 아닌 그들이 보는 사실과 다를바 없는 리얼리티이기 때문이다. 2013.09.19 11:30
    3 0025212272003 그 전의 재현이라는 리얼리티를 깨고 새로우 방식으로 추구하였는데 사물을 다각면으로 이해하고 사물의 본질. 자연에 가려지 다양한 측면을 찾기위해 끊임없이 소통했다.
    그러면서 자연이라는 측면과 사물이라는 측면 모두 잃지 않았다.
    2013.09.19 18:14
    4 2012049730 이 말은 예술 작품이 작가가 아니라 독자에 의해 감상되고 평가된다는 점에 착안한 것이다. 그렇기 때문에 감상자의 주관적이고 개성적인 평가를 인정한다. 이는 예술 감상에 있어서 필수불가결적 양상이라고 생각한다. 2013.09.20 16:25
    5 000620133706 .. 2013.09.21 17:10
    6 2009050950 메를로 퐁티는 화가가 "대부분의 사람들이 세계에 묻혀 보지 못하는 광경을 포착하여 보여주는 사람"이라고 말했다. 이 말을 다시 짚어보면 세잔의 회화는 일반적인 사람이 보는 시선에서 벗어나 그들이 보지 못한 현실을 포착하여 보여주는 것이라 볼 수 있다. 일반적인 사람들이 보지 못한 현실은 허구가 아닌 그들이 보는 사실과 다를바 없는 리얼리티이기 때문이다. 2013.09.22 22:50
    7 0013200838054 리얼리티라는 생각을 해보면 메를로퐁티의 생각을 존중하는것이 제일 큰 의미라고생각한다 그이유는 일반적인 시선과 다른 또 다른 시선으로 볼수있기를 원하는 모습을 그려내는것이라고 생각합니다 2013.09.23 04:23
    8 00032008720184 수단을 버리고 리얼리티를 추구했다는 말은 메를로 퐁티는 화가가 "대부분의 사람들이 세계에 묻혀 보지 못하는 광경을 포착하여 보여주는 사람"이라고 말했다. 이 말을 다시 짚어보면 세잔의 회화는 일반적인 사람이 보는 시선에서 벗어나 그들이 보지 못한 현실을 포착하여 보여주는 것이라 볼 수 있다. 일반적인 사람들이 보지 못한 현실은 허구가 아닌 그들이 보는 사실과 다를바 없는 현실이라 생각했기 때문이다 2013.09.23 15:07
    9 0025212122008 세잔은 사실적으로 내고자했다. 2013.09.24 10:37
    10 0010201011237 감각적인 표현이 두드러진다 2013.09.24 22:46
    작성자 의견 작성일
    11 000620081723 원근법적이거나 재현적인것을 따라 한 것이 아닌 세잔이 생각하는 사실적인것을 표현하고자 노력하였다. 2013.09.25 00:16
    12 0013201270035 진짜 진짜인걸 보자 2013.09.25 01:28
    13 0025211010005 많은 사람들이 그림을 통해 귀감을 얻을 수 있도록 연구해야하는 것이 화가라고 생각한 세잔은 예술은 자연을 표현하되 그것이 계속 존속되어야 한다고 생각함 2013.09.25 16:04
    14 0013201202011 대부분 사람들이 세계에 묻혀 보지 못하는 광경을 포착해서 보여주는 사람이라고 하였는데 이것은 풍경의 구조를 탄생하는 유기체로 포착하여 사물 하나하나가 가진 숭고한 지속을 구현해내기 때문이다. 그래서 세잔은 리얼리티에 이르는 수단을 버리고 리얼리티를 추구하고자 한 것이다. 2013.09.25 16:05
    15 00032006317082 사실적으로 다가온다라는 것에 중점을 둔것이며, 극사실주의적 표현방법에 집착하기 보다는 회화가 풍기는 느낌의 사실됨을 담고자 한 것이 세잔느의 회화가 가지는 리얼리티라고 하겠습니다. 2013.09.25 18:28
    16 2007005545 리얼리티를 그 자체로 이해해야 한다고 생각했다. 2013.09.25 19:28
    17 0025211100070 '풍경이 내 안에서 그 자체를 생각하고, 나는 그것의 의식이다” 세잔은 ‘리얼리즘’을 추구한다고 말하는데 그가 말하는 리얼리즘은 인간이 사물들의 진리를 포착하기 위해 일상성을 멈출 수 있다는 것을 가정하고 있다. 다시 말하면 단지 인간적인 사색만이 근본적인 지각에 도달할 수 있으며 자연은 인간 정신의 사유를 통해서만 그 자의식에 도달할 수 있다는 것이다. 세잔이 갖는 회의의 핵심은 메를로퐁티에 따르면 ‘표현’으로서의 예술의 기능이다. 2013.09.27 14:06
    18 00032009405002 메를로 퐁티는 리얼리티를 추구하는데 있어서 리얼리티에이르는 수단 즉 , 대부분의 사람들이 리얼리티를 인식하는 방식에서 벗어나서 리얼리티를 추구해야한다는 또다른 현식인식의 방법을 사용하여 리얼리티를 추구했다고 볼 수 있다고 생각합니다. 2013.09.28 18:21
    19 00032012903032 메를로 퐁티는 "화가란 대부분의 사람들이 세계에 묻혀 보지 못하는 광경을 포착하여 보여주는 사람"이라고 했습니다. 세잔이 단순한 리얼리티를 추구한것이 아니라 빛과 색에 주력하여 대상을 포착하고, 그것을 자신만의 질서로 구현했기 때문에 세잔의 회화가 리얼리티에 이르는 수단을 버리고 리얼리티를 추구하고자 했다라고 한것 같습니다. 즉, 세잔이 풍경의 구조를 탄생하는 유기체로 포착하여 사물 하나하나가 가진 숭고한 지속을 구현해냈다는 것입니다. 2013.09.28 19:03
    20 00032007404002 자연에서 나오는 '숭고한 지속'을 표현하기 위해서 세잔은 감각에서 얻어지는 빛과 색과 같은 인상(리얼리티에 이르는 수단을 포기)을 통해서 사물을 견고하게 만들어(리얼리티를 추구) 자연의 모습을 있는 그대로 표현해 냈다고 생각한다. 2013.09.28 19:24



글예 5강.


  • 근대의 몸과 이성의 이분법에 대해 설명할 수 있다.
  • 근대 미술의 흐름에 대해 이해하고 설명할 수 있다.
  • 근대 사회에서 이해된 예술의 의미에 대해 설명할 수 있다.


  1. 몸과 이성에 대한 이해방식의 변화
    1. 근대의 몸과 이성의 이분법
      1. 전근대인들은 독자적으로 존재 아님. 태생적으로, 당위적으로 세계를 경험함. 카톨릭의 교류에 따라 살아가는것.  전근대 말기 이런한것들이 상실됨. -> 
      2. 근대적 주체의 탄생 -> 자기 자신!
        1. '나는 사유한다, 그러므로 존재한다.' - 데카르트
      3. 사유와 인식의 절대적 특권과 정당성을 부여 받은 주체
      4. 타자를 표상함으로써 자기를 인식하는 주체.
        1. 인식하는 주체와 인식과는 타자와 위계적 이분법 확림
      5. 내가 생각하고있다는 그 순간은 아무리 의심을해도 , 생각하는 나는 부정할 수 없게 됨. 
      6. (이미지1)
        1. 인간은 얼마든지 자연을 이용할 수 있는것으로 특권을 받았당.
        2. 여성은 자연을 뜻함.
        3. 근대의 정신을 주체의정신으로 이해할 수 있다
    2. 근대 계몽주의
      1. 촛불과해골그림
        1. 촛불 - > 진리의 빛. 미성숙의 상태를 성숙의 상태로 바꿔주는 계몽의 빛.
        2. 해골 - > 인간으로서 가치가 아주 폄하됐을 때, 단지 해골로서만 존재한다. 
      2. 로뎅 : 근대조각의 아버지.
        1. 생각하는 남자 조각 
          1. 근대조각의 태도를 자신의 조각으로 보여줌.
        2. 여성 조각.
          1. 감각 그 자체. 매끄 , 부드럽고, , 이성적인 사유의 능력이 나타나있지않다.
        3. 근대 조각에 있어서 여성과 남성의 성적인 차이가 드러나게 된다. 
        4. 단순히 성으로 나뉜게 아니라 도식적으로 분리돼있다.
      3. 사유하는 사람 ( 서양, 동양)
        1. 로뎅의 조각품 -> 크기가 상당히 큼. 로뎅의 사유하는 심각한 얼굴. 아주 남성적인 근육들. 눌린 입술.
        2. 금동미륵반가사유상 -> 남/여인지 중요치않아. 생각하는. 살짝 미소. 육체와 정신이 분리되어있찌 않다는 것을 보여주고있다.
    3. 근대의 자화상
      1. 설명
        1. 근대적 자의식이 반영된 근대의 산물
        2. 화가가 의뢰자의 요구에 따라 그림을 그리는 장인이 아니라, 천재로 인식됨.
        3. '나는 누구인가?'에 대한 탐구
        4. 거울에 비친 자신을 바라보는 반성의 구도.
        5. 거울에 비친 자신을 바라보는 반성의 구도
        6. 자신을 응시하는 또 다른 자신의 시선
        7. 반성하는 자와 반성되는 자가 무한히 서로 반영되는 통일적 구도가 형성 됨.
        8. 자화상, 거울, 반성적인 삶 -> 근대의 주체정신을 그대로 드러낸다.
        9. 거울 중요시여겨짐.
    4. 램브란트
      1. 영혼을 그릴수 있는 작가.
  2. 근대 미술의 흐름
    1. 매너리즘 > 바로크 > 로코코 > // 신고전주의 > 낭만주의 > 사실주의
      1. 매너리즘
        1. 르네상스 전성기 말 16세기 중엽~ 17세기 초
        2. 완전한 미를 추구한 르네상스 미술에 대한 돌파구에 대한 모색
        3. 고전주의 잣대로 평가할 수 없는 일탈과 변형 시작.
        4. 르네상스와 바로크를 잇는 교량 역할.
        5. 의도적 부조화 / 기괴한 배경 / 과장된 인체비례 / 불안감
      2. 바로크
        1. 17-18세기 중반, 이탈리아 로마를 중심으로 발생.
        2. 남성적인모습 
        3. 빛의 강렬한 대비를 통한 장중함
        4. 위엄성. 
        5. 빛은 중세시대에도 중심이었지만 그 당시에는 보이지않는 빛!
        6. 대비를 강조했다. 
        7. 카라 바지오 루벤스, 램브란트
        8. 어떤의미에선 운동감을 느낄수있음. 긴장감도 느껴짐.
      3. 로코코
        1. 프랑스로 중심으로 발전한 양식.
        2. 베르사유 궁전. 사치스럽고, 화려함.
        3. 현실적이기보다는 우아하고 몽환적, 연회하는 귀족의 그림.
        4. 섬세, 정교, 우아한 세련미를 통해 바로크의 남성적 화풍을 견제, 허영미. 상당히 귀족쩌어얶
        5. 작가 : 와타
      4. ======== 로코코까지는 양식. 한시대를 풍미함! 이 이후엔 한 시대를 압도한게 아니라 -ism 이다. ~주의임. 복합적 양상으로이해를해야한다.
      5. 신고전주의
        1. 로코코 시대 예술의 허영과 세속적 화풍에 대한 반감
        2. 내면을 사유하게 하는 고귀한 인간 행위에 대한 진지한 주제
        3. 예능은 그만! 토론, 다큐하자! 와같음.
        4. 비례적이고 이상에맞춘. 그리스적인 회긔긔긔
        5. 고전주의 시대에 화풍의 영향
        6. 작가 : 다비드, 앵그르(중요)
        7. 다비드의 서명행위 -> 근대적임. 정치적인.
      6. 낭만주의.
        1. 성향이 다양하다.
        2. 18-19세기 유럽 질풍노도의 시기(정치적 혁명) (프랑스 혁명 ~)
        3. 신 고전주의, 이성주의 주장들에 반비례해서 오히려 그것 내면에 있는 무의식적인 질서. 이상향. 상성적 부분. 우리와 현실적 논리와 다른 논리가 작동하고있다.
        4. 시적이고 신화적 주제(현실을 떠난 상상력)
        5. 현실 떠나 이상향 추구, 이국적 소재에의 관심(현실도피적 성격)
        6. 감정과 작가의 상상력 중요, 자연에 대한 귀의 경향
        7. 색채에 비중을 두고 화려하고 열정적인 화면을 구사
        8. 진지한것 다루는게 아니라 부글부글 끓는 것을 표현.
        9. 작가 : 고야, 들라크루아, 터너. 낭만주의작가 굉장히 많다. 
          1. 고야(Goya) : 
            1. 왕실소속 작가. 
            2. 왕가의 그림을 그려냄. 
            3. 도도한 여왕그림. 
            4.  괴물에게 잡아먹히는그림..
            5.  한명이 손을 들면서 총살을 저지시키는그림. 
            6. 다양한 스펙트럼을 소화해냄.
            7. 주제가 굉장히 다양함!
          2. 블레이크(Blake):
            1. 영국작가.
            2.  압도적인 느낌. 신화적.인 표현. 낭만주의작가
          3. 터너(Turner):
            1. 영국인이 가장 사랑하는 작가
            2. 바다를 배경으로한 그림. -> 자연을 배경.
            3. 상어떼에게 묵여뜯기고, 바다가 피로 물듬. 노예선이 난파됨. 노예는 수갑에 차여있어 도망도못가고 상어밥이됨.
            4. 정치사에 있어서 정치적 함축을 가진다.
      7. 사실주의
        1. 낭만주의만큼 복잡하진않다.
        2. 아주심플하지만 아주 분명했다.
        3. 1855년 쿠르베의 개인전 '레알리즘'에서 유래.
        4. 앵그르 : 신고전주의때 아주 이상적인 비례를 통해 여성의 몸을 보여주엇던 이상적인 작가. 부르주아들이 좋아하는 그림.
          1. 천사도 그리고 고귀한 주제들. 많은 작품들을 그려냈음.
        5. 쿠르베 : 나는 천사를 그리지 않는다. 
          1. 눈에 보이는, 과학적인, 경험에의한, 그러한 것들을 그려내겠다는 의지.
        6. 회화의 주제는 고상하다는 당대규범에 저항, 노동자와 평민 그림.
        7. 19세기 프랑스 '과학주의적 태도'의 영향, 근대정신의 발현
        8. 현실에 대한 정직한 기록이자 현실의 규명이며 세계관의 반영
        9. 눈에 보이는것만 믿겠다.(천사? 관념적으로만 존재하는 아름다운 것들. 은 노노노노노노. )
        10. 작가 : 쿠르베 , 밀레.
          1. Bonjour Courbet :  화가,작가가 어떻게 대우받아야하는지 보여주는 작품.
          2. 밀레 :
            1. 일상의 일하는 모습. 이삭줍기. > 부르주아들이 싫어함
            2. 사회 전반적으로 환영받지는 않았다.
  3. 근대사회에서 이해된 예쑬의 의미.(까장중요)
    1. 예술 : < 자유의 실현과 사회통합체를 가능하게 하는 매개체의 역할을 부여받음. 자유긴 자유지만, 망나니같은 자유가아닌 사회를 통합하도록 노력하는 자유! -> 갭을 예술과 교양을 통해 인위적으로 줄여나가야함.
    2. 개인에게 부여된 이중의 과제.
      1. 자유의 실현과 사회통합의 구성원으로서의 개인
      2. 개인이 자유를 실현함과 동시에 시민국가라고 하는 이 공동체 질서 안에서 함게 사회를 만들어나가는.  함께살기의 구성원. 개인에게 이중으로 부과됨.
      3. 사실 이 두가지가 붙기가 굉장히힘든것.
        1. 생산활동을 통해 사적인 자유를 행할 수 있어. 그러나 사회활동안에서~?
        2. >> 좁히려는 노력. 그냥은 안돼. 교육을 시켜야함! 시민은 노력을 해야 시민이된다. 
    3. 불가능한 양자의 거리를 예술, 교육을 통해 인위적으로 통합시킴
      1. 교양으로서의 예술
      2. 자유시민예술. 
      3. 근대인들에게 교양? : 개인의 자유와 사회통압을 묶어내기위한 그들이 가지는 어떠한 가치관, 세계관. 그것을 완성시키는것이 교양이다. -> 가장 효과적인 도구는 예술!. ( 난파선,, 밀레 씨앗뿌리는사람들 …  >> 유럽사회는 모더니즘을 향해 계속 진보(사회, 과학, 기술적 모더니즘) . 직선적으로 나아가는 모더니즘을 '어 우리 맞게가고있는거야?'하고 비판할수 있는게 바로 예술.
      4. 이러한 비판을 하는게 바로 시민.
    4. 진보와 발전으로 제시되는 근대 사회에서 예술
i/themes/dunet/jquery-ui-1.8.custom.css?v=1.1">
학습제목 근대적 주체와 예술의 흐름
주제 서구 근대가 이해하는 몸과 정신에 대한 입장은 매우 단호한 것이었다. 하지만 몸은 감각적 연장으로 정신은 인간의 본질로서 이해하는 서구 근대적 방식과는 대조적으로 오히려 몸이 인간 본질의 근간으로 이해될 수 있는 구체적인 경험들을 공유해보자. (예를 들어 정확한 기온을 이성적으로 인지하지 못하더라도, 우리의 신체적 감각이 이미 계절의 도래를 알고 있는 경우)
No 작성자 의견 작성일
1 0010201010232 주변 사람이 무엇때문에 기분이 안 좋은지 모르더라도 그 사람의 인상이나 주변의 분위기로 무슨 일이 있구나라는 것을 먼저 감각적으로 느끼는 경우 2013.09.18 10:06
2 00032013405001 긴급한 상황은 이성보다 몸이 더 먼저 인지한다. 2013.09.19 08:21
3 0025212272003 음식 섭취 시 내가 얼마만큼의 양으로 배가 차는지 숫자적으로는 모르지만 배가 부름을 안다. 2013.09.19 16:48
4 2012049730 감기의 경우, 실제 온도의 차이가 아니라 체감온도의 차이때문에 발병한다. 이를 통해 절대적인 온도의 변화보다 인간이 느끼는 것이 더 신체에 중요함을 알 수 있다. 이는 몸이 인간 본질의 근간으로 이해될 수 있는 경험으로 볼 수 있다. 2013.09.20 16:22
5 0002201212257 자주 웃는것이 이성적으로는 우리몸에 이롭다는것을 인지하지 못하지만, 신체 생리학적으로는 유익한 호르몬이 나오고 도움이 되고있는 경우 2013.09.21 14:25
6 0015103579 신체는 아름다운 것이며 매우 중요하다.
신체의 언어. 즉 신체는 또다른 언어라고도 할수있다.
2013.09.21 18:29
7 00032013742064 속이 보이지 않는 상자에 있는 물건을 알아 낼때, 그 무엇보다 만지는 촉감이 중요하다. 2013.09.22 12:51
8 00032007404002 길가에 핀 꽃을 보았을때 향기를 맡지 않고도 그 꽃의 향기를 알고 있을 때 2013.09.22 14:23
9 2009050950 감기의 경우, 실제 온도의 차이가 아니라 체감온도의 차이때문에 발병한다. 이를 통해 절대적인 온도의 변화보다 인간이 느끼는 것이 더 신체에 중요함을 알 수 있다. 이는 몸이 인간 본질의 근간으로 이해될 수 있는 경험으로 볼 수 있다. 2013.09.22 22:37
10 0013200838054 제일 쉽게 볼수잇는 얼굴에서 표정으로 많은 것을 알수 있다. 기분에 따라서 웃는 상과 우는상 그 밖에 많은 변화가 표정으로 나오는 것을 볼수있다. 2013.09.23 06:07
11 0025212122008 계절이 바뀔 때마다 알레르기성 비염이 옵니다. 2013.09.23 10:28
12 000620081723 배가 고프거나, 갈증이 나거나 또는 생리적인 현상들은 이성을 무너트린다. 2013.09.23 11:20
13 000620134060 가끔 예지몽 같은 꿈은 꾸었을때 어떠한 일이 생기는 것에 대한 낯설지 않은 반응 2013.09.23 13:53
14 00032008720184 열이난다면 객관적으로는 체온을 알 수 없지만 몸이 뜨겁다고 느낄 수 있다. 2013.09.23 15:02
15 0013201270035 맛있는걸 보면 입맛을 다신다 2013.09.24 13:40
16 0025212140036 레몬을 보고 있으면 굳이 먹지 않아도 시큼하다는 생각이 들며 침샘을 자극하여 입에 침이 고이게 되고, 빨간 음식을 보면 매운맛이 절로 느껴지며 땀이 나게 된다. 2013.09.25 10:24
17 0025211010005 배고픔을 느낀느 것은 신체적 신호가 왔을때 아는 것이고 머리로 먼저 배고픔을 느끼진 않는다. 2013.09.25 12:37
18 0013201202011 배가 고프거나 갈증이 났을때 머리로는 인식이 되지않고 신체적으로 감각이 났을때 인지하는 경우이다. 2013.09.25 14:38
19 00032006317082 상황이 닥치기 전에 을씨년스러운 느낌이 들었을 때, 그리고나서 실제로 어떠한 나의 구체적인 행동을 취해야 되는 사고나 상황이 벌어졌을 때, 신체의 인지가 선행하는 장면을 경험하게 된다. 2013.09.25 16:07
20 0002201010449 일기예보가 틀렷을때도 몸이 날씨의 변화를 느끼는 경우 2013.09.25 17
21 000620091016 스쳐지나가는 물체를 제대로 인식하지 못해지만 어떤것인지 알경우 2013.09.25 17:51
22 0025211100070 어르신들이 비오기전날 어떤얘기를듣지 않았어도 무릎이 쑤시거나 통증을느껴 다음날 비가올 것이라고 하는경우를 예로들수있다. 2013.09.26 17:53
23 00032009730052 뜨거운 주전자를 만졌을 때 뜨겁다고 느끼기 전부터 손을 떼는 것 등등 2013.09.27 16:07
24 00032009405002 무언가 시간을정하고 일을 할때 그시간만큼 지났을때 저의 몸이 알아서 그시간만큼이 경과함을 알려줄때가 있는것 같습니다. 2013.09.27 18:48
25 00032012903032 병원에 가지 않더라도 몸이 으슬으슬한다던가 등의 증상으로 이미 감기가 왔음을 알고있는 경우 2013.09.28 16:11
26 0025212270015 레몬을 보고 있으면 먹지 않아도 시다는 생각이 들며 침샘을 자극하여 입에 침이 고인다. 2013.09.28 16:43
27 00032009302025 몸은 우리가 이성적으로 시간이 언제인지 몰라도 일어나야 하는 아침, 밥 먹어야 하는 점심, 잠을 자야하는 저녁을 알려준다. 2013.09.28 17:14
28 2009040937 서구 근대 사회에서는 인간이 우선시 됨에 따라 인간의 이성을 중시하게 되었다. 하지만 이는 명확히 분리 될 수 있는 단순한 문제가 아니다. 즉, 신체가 본능적으로 느끼는 부분이 분명히 존재한다는 뜻이다.

가령 해외여행시 느껴지는 시차부적응이 그 대표적인 예가 될 것이다. 이성적으로는 현지가 밤이고, 고국이 낮인 상황을 이해하면서도, 시차에 잘 적응하지 못하는 경우에는 현지의 낮에 자게되고, 밤에 깨어 활동하게 된다.
2013.09.28 18:03
29 0010201120767 공부를해야 하는 상황이지만 밤을 이틀이나 샛다면 아무리 졸지 않고 잡을 세려해도 잠이 오는 것 2013.09.28 18:45
30 0015112336 가장 대표적인 예로 레몬을 먹지 않고 보기만 해도 시다는 것이 인식되어 침이 넘어가는 것이 있습니다. 2013.09.28 22:22








List 타입인 'Entry' <span> {{ Entry|length }} </span>

<span> {{ Entry|count }} </span>


이런방식으로 쓰면됨. 


출처 : http://stackoverflow.com/questions/1465249/jinja2-get-lengths-of-list




개발환경 할 때마다 찾아보게 되어 정리해둡니다. (update 2013.08.12)



1. 이클립스 다운로드



2. 안드로이드 sdk 다운로드



Get the SDK 로 이동



이클립스IDE는 받았으므로, SDK만.



3. 안드로이드 sdk 경로를 이동

[터미널] (다운로드 폴더에 받은 경우)
$ : cd downloads 
$ : mv android-sdk-macosx /Users/coz/library


[파인더에서 이동하기]

android-sdk-macosx 폴더 복사 후

상단 바의 이동을 누르고, 키보드의 [옵션]버튼을 누르면 [라이브러리] 항목이 생김.
클릭 및 폴더 복사




4. 경로 설정


[터미널]
$ : cd
$ : vi .bash_profile
$ : i (입력모드)

vi 편집기로 bash_profile이 열립니다. i를 누르면 편집모드가 됩니다.

아래의 경로를 입력 후

export ANDROID_SDK_ROOT="/Users/유저네임/library/android-sdk-macosx"
export PATH="$PATH:$ANDROID_SDK_ROOT/tools"


esc 누르고
:wq ( 저장 후 종료 타이핑 후 엔터)

$ : source ~/.bash_profile (적용)




5. 이클립스 실행 (자바 런타임 없으면 설치 팝업을 통해 설치)



Help -> install new software

Work with 에 다음 경로 입력 후 엔터

http://dl-ssl.google.com/android/eclipse/

기다리면 항목에 Developer Tools 생기는데 체크 후 Next





Android SDK Manager 실행 후 SDK설치 내역을 확인.
필요한 버전 다운로드


설치 후 Help->check for updates로 업데이트



6. 안드로이드 타겟 확인

정상적으로 설치됐는지 테스트
[터미널]
$ : android list target

성공



참고)

이클립스에서 File->New->Android Project 가 바로 보이도록 설정하기

이클립스의 Window-> Customize Perspective-> Shortcuts-> android체크







프로젝트 생성 후 실행















AVD 생성







테스트 프로젝트 실행









완성공





현재 최신 버전은 3.0 @ 이지만, 
[저는 2.1.4 버전 프로젝트 진행중이므로 본 포스팅은 2.1.4로 진행하겠습니다.]



압축 해제 후 개발 폴더로 이동

[저는 /Users/cozdebrainpower/Desktop/ 으로 복사하였습니다.]









현재 맥 최신 버전 : android-ndk-r9-darwin-x86_64.tar.bz2 클릭

2.1.4에서 android-ndk-r9 으로 컴파일하면 에러가 많이 뜨네요.
android-ndk-r8e로 진행하겠습니다.
update: 13.08.18 ndk-r9으로 진행하겠습니다.

압축 해제 후 Library폴더로 이동
(터미널 사용 시 :  $mv android-ndk-r8e /Users/cozdebrainpower/library )

현재 설치 구조
Desktop에는 eclipsecocos2d-x-2.1.4 폴더가 존재 
Library에는 android-sdk-macosx, android-ndk-r9 가 존재


3. 환경변수 입력


[터미널]
$ vi .bash_profile 편집기를 열어서

i 누르고, 다음 경로를 입력 후 저장

1번 포스팅 단계에서 이미 다음과 같이 저장해둔 상태일 것이다.
export ANDROID_SDK_ROOT="/Users/coz/library/android-sdk-macosx"
export PATH="$PATH:$ANDROID_SDK_ROOT/tools"

추가.
export ANDROID_NDK_ROOT="/Users/cozdebrainpower/library/android-ndk-r9"
export COCOS2DX_ROOT="/Users/cozdebrainpower/Desktop/cocos2d-x-2.1.4"
export NDK_ROOT="/Users/cozdebrainpower/library/android-ndk-r9"
export PATH=$PATH:$ANDROID_NDK_ROOT
export PATH=/opt/local/bin:/opt/local/sbin:$PATH

esc누르고 :wq입력 (저장 후 종료)

$ source ~/.bash_profile






4. xcode에 cocos2d-x 템플릿 설치


cocos2d-x가 설치된 폴더로 이동 (Desktop/cocos2d-x-2.1.4/)

template 설치
$ sudo ./install-templates-xcode.sh

WARNING: Improper use of the sudo command could lead to data loss
or the deletion of important system files. Please double-check your
typing when using sudo. Type "man sudo" for more information.

To proceed, enter your password, or type Ctrl-C to abort.

Password: 맥북 비밀번호 입력
cocos2d-x template installer





xcode 에서 템플릿 확인


완성공


5. 멀티플랫폼 프로젝트 생성

기존의 프로젝트를 만들고, 폴더를 병합하던 노가다 방식에서 벗어나, 파이썬을 활용한 멀티플랫폼 프로젝트 생성입니다.

[터미널]
cocos2d-x-2.1.4/tools/project-creator/ 폴더로 이동.
./create_project.py 을 입력해봅니다.



3가지를 입력해야 하는데, 
-project  생성할 프로젝트명
-package  생성할 프로젝트의 패키지명
-language 사용할 개발 언어

개발언어는 뒤에 cpp | lua | javascript 중 선택하게 되어있습니다.


저는 c++로 개발할 것이기 때문에 다음과 같이 입력했습니다.

./create_project.py  -project MyGame -package com.company.mygame -language cpp




cocos2d-x-2.1.4 폴더 안에 projects 폴더가 생성되고, 입력한 프로젝트가 다양한 플랫폼으로 생성됩니다.



6. iOS 프로젝트 실행




완성공


7. 안드로이드 프로젝트 실행

[터미널]
cocos2d-x-2.1.4/projects/MyGame/proj.android/ 로 이동

./build_native.sh  실행
하면 에러가 뜰 텐데요 (현재는.)
라이브러리를 수정해서 저 에러를 수정해도 다른 에러는 계속 발생합니다.

다음을 수정합니다.
proj.android/jni/Application.mk 파일을 오픈합니다.

그리고 다음과 같이 한 줄 추가해주세요.
APP_CFLAGS += -Wno-error=format-security



다시 ./build_native.sh  실행


완성공.



이클립스 실행



프로젝트 불러오기










cocos2d-x 라이브러리 임포트



2.0.3부터는 이클립스에서 프로젝트 만들어도 바로 실행되지 않습니다.
cocos2d-x lib가 외부 프로젝트로 되어있어서 임포트를 해야하는데요.
이클립스 File->Import에서 General-Existing Projects into Workspace로 다음의 프로젝트 경로를 임포트합니다.
cocos2d-x-2.1.4/cocos2dx/platform/android/java 폴더를 임포트 합니다.
임포트 후 이클립스 workspace에는 libcocos2dx 프로젝트가 생성되고, 정상적으로 실행이 가능합니다.




--------------------- 이전 버전 안내 포스트--------------------- 

4. android 프로젝트 생성

Xcode에서는 cocos2dx 프로젝트를 만들수 있는 템플릿을 설치했고, 
안드로이드의 경우 cocos2dx가 설치된 폴더에서 직접 프로젝트를 생성해줘야 합니다.
(우리의 경우에는 desktop/cocos2d-x 폴더안에 있겠죠)

프로젝트 생성
Kwon-ui-MacBook-Pro:cocos2d-x coz$ ./create-android-project.sh  엔터  (create-android-project.bat 는 윈도우용 배치파일)

정의된 sdk, ndk 경로가 표시됩니다.
use global definition of NDK_ROOT: /Users/coz/library/android-ndk-r8d
use global definition of ANDROID_SDK_ROOT: /Users/coz/library/android-sdk-macosx
Input package path. For example: org.cocos2dx.example <= 만들 안드로이드 프로젝트 패키지명을 요구
com.ohjung.cocos2dxtest


안드로이드 빌드타겟을 설정합니다.
본인이 세팅한 안드로이드 개발환경에 따라서 다릅니다.
원하는 android 타겟의 아이디값을 입력

Now cocos2d-x supports Android 2.2 or upper version
Available Android targets:
----------
id: 1 or "android-7"
     Name: Android 2.1
     Type: Platform
     API level: 7
     Revision: 3
     Skins: HVGA, QVGA, WQVGA400, WQVGA432, WVGA800 (default), WVGA854
     ABIs : armeabi
----------
id: 2 or "Google Inc.:Google APIs:7"
     Name: Google APIs
     Type: Add-On
     Vendor: Google Inc.
     Revision: 1
     Description: Android + Google APIs
     Based on Android 2.1 (API level 7)
     Libraries:
      * com.google.android.maps (maps.jar)
          API for Google Maps
     Skins: WVGA854, WQVGA400, HVGA, WQVGA432, WVGA800 (default), QVGA
     ABIs : armeabi
----------
id: 3 or "android-8"
     Name: Android 2.2
     Type: Platform
     API level: 8
     Revision: 3
     Skins: HVGA, QVGA, WQVGA400, WQVGA432, WVGA800 (default), WVGA854
     ABIs : armeabi
----------
id: 4 or "Google Inc.:Google APIs:8"
     Name: Google APIs
     Type: Add-On
     Vendor: Google Inc.
     Revision: 2
     Description: Android + Google APIs
     Based on Android 2.2 (API level 8)
     Libraries:
      * com.google.android.maps (maps.jar)
          API for Google Maps
     Skins: WVGA854, WQVGA400, HVGA, WQVGA432, WVGA800 (default), QVGA
     ABIs : armeabi
----------
id: 5 or "android-10"
     Name: Android 2.3.3
     Type: Platform
     API level: 10
     Revision: 2
     Skins: HVGA, QVGA, WQVGA400, WQVGA432, WVGA800 (default), WVGA854
     ABIs : armeabi
----------
id: 6 or "Google Inc.:Google APIs:10"
     Name: Google APIs
     Type: Add-On
     Vendor: Google Inc.
     Revision: 2
     Description: Android + Google APIs
     Based on Android 2.3.3 (API level 10)
     Libraries:
      * com.android.future.usb.accessory (usb.jar)
          API for USB Accessories
      * com.google.android.maps (maps.jar)
          API for Google Maps
     Skins: WVGA854, WQVGA400, HVGA, WQVGA432, WVGA800 (default), QVGA
     ABIs : armeabi
----------
id: 7 or "Intel Corporation:Intel Atom x86 System Image:10"
     Name: Intel Atom x86 System Image
     Type: Add-On
     Vendor: Intel Corporation
     Revision: 1
     Description: Intel Atom x86 System Image
     Based on Android 2.3.3 (API level 10)
     Skins: WVGA854, WQVGA400, HVGA, WQVGA432, WVGA800 (default), QVGA
     ABIs : x86
----------
id: 8 or "android-11"
     Name: Android 3.0
     Type: Platform
     API level: 11
     Revision: 2
     Skins: WXGA (default)
     ABIs : armeabi
----------
id: 9 or "Google Inc.:Google APIs:11"
     Name: Google APIs
     Type: Add-On
     Vendor: Google Inc.
     Revision: 1
     Description: Android + Google APIs
     Based on Android 3.0 (API level 11)
     Libraries:
      * com.google.android.maps (maps.jar)
          API for Google Maps
     Skins: WXGA (default)
     ABIs : armeabi
----------
id: 10 or "android-12"
     Name: Android 3.1
     Type: Platform
     API level: 12
     Revision: 3
     Skins: WXGA (default)
     ABIs : armeabi
----------
id: 11 or "Google Inc.:Google APIs:12"
     Name: Google APIs
     Type: Add-On
     Vendor: Google Inc.
     Revision: 1
     Description: Android + Google APIs
     Based on Android 3.1 (API level 12)
     Libraries:
      * com.android.future.usb.accessory (usb.jar)
          API for USB Accessories
      * com.google.android.maps (maps.jar)
          API for Google Maps
     Skins: WXGA (default)
     ABIs : armeabi
----------
id: 12 or "android-13"
     Name: Android 3.2
     Type: Platform
     API level: 13
     Revision: 1
     Skins: WXGA (default)
     ABIs : armeabi
----------
id: 13 or "Google Inc.:Google APIs:13"
     Name: Google APIs
     Type: Add-On
     Vendor: Google Inc.
     Revision: 1
     Description: Android + Google APIs
     Based on Android 3.2 (API level 13)
     Libraries:
      * com.android.future.usb.accessory (usb.jar)
          API for USB Accessories
      * com.google.android.maps (maps.jar)
          API for Google Maps
     Skins: WXGA (default)
     ABIs : armeabi
----------
id: 14 or "Google Inc.:Google TV Addon:13"
     Name: Google TV Addon
     Type: Add-On
     Vendor: Google Inc.
     Revision: 1
     Based on Android 3.2 (API level 13)
     Skins: WXGA, 1080p-overscan, 1080p, 720p-overscan, 720p (default)
     ABIs : x86
----------
id: 15 or "android-14"
     Name: Android 4.0
     Type: Platform
     API level: 14
     Revision: 3
     Skins: HVGA, QVGA, WQVGA400, WQVGA432, WSVGA, WVGA800 (default), WVGA854, WXGA720, WXGA800
     ABIs : armeabi-v7a
----------
id: 16 or "Google Inc.:Google APIs:14"
     Name: Google APIs
     Type: Add-On
     Vendor: Google Inc.
     Revision: 2
     Description: Android + Google APIs
     Based on Android 4.0 (API level 14)
     Libraries:
      * com.android.future.usb.accessory (usb.jar)
          API for USB Accessories
      * com.google.android.maps (maps.jar)
          API for Google Maps
     Skins: WVGA854, WQVGA400, WSVGA, WXGA720, HVGA, WQVGA432, WVGA800 (default), QVGA, WXGA800
     ABIs : armeabi-v7a
----------
id: 17 or "android-15"
     Name: Android 4.0.3
     Type: Platform
     API level: 15
     Revision: 3
     Skins: HVGA, QVGA, WQVGA400, WQVGA432, WSVGA, WVGA800 (default), WVGA854, WXGA720, WXGA800
     ABIs : armeabi-v7a, mips, x86
----------
id: 18 or "Google Inc.:Google APIs:15"
     Name: Google APIs
     Type: Add-On
     Vendor: Google Inc.
     Revision: 2
     Description: Android + Google APIs
     Based on Android 4.0.3 (API level 15)
     Libraries:
      * com.google.android.media.effects (effects.jar)
          Collection of video effects
      * com.android.future.usb.accessory (usb.jar)
          API for USB Accessories
      * com.google.android.maps (maps.jar)
          API for Google Maps
     Skins: WVGA854, WQVGA400, WSVGA, WXGA720, HVGA, WQVGA432, WVGA800 (default), QVGA, WXGA800
     ABIs : armeabi-v7a
----------
id: 19 or "android-16"
     Name: Android 4.1.2
     Type: Platform
     API level: 16
     Revision: 4
     Skins: HVGA, QVGA, WQVGA400, WQVGA432, WSVGA, WVGA800 (default), WVGA854, WXGA720, WXGA800, WXGA800-7in
     ABIs : armeabi-v7a, mips, x86
----------
id: 20 or "Google Inc.:Google APIs:16"
     Name: Google APIs
     Type: Add-On
     Vendor: Google Inc.
     Revision: 3
     Description: Android + Google APIs
     Based on Android 4.1.2 (API level 16)
     Libraries:
      * com.google.android.media.effects (effects.jar)
          Collection of video effects
      * com.android.future.usb.accessory (usb.jar)
          API for USB Accessories
      * com.google.android.maps (maps.jar)
          API for Google Maps
     Skins: WVGA854, WQVGA400, WSVGA, WXGA800-7in, WXGA720, HVGA, WQVGA432, WVGA800 (default), QVGA, WXGA800
     ABIs : armeabi-v7a
----------
id: 21 or "android-17"
     Name: Android 4.2
     Type: Platform
     API level: 17
     Revision: 1
     Skins: HVGA, QVGA, WQVGA400, WQVGA432, WSVGA, WVGA800 (default), WVGA854, WXGA720, WXGA800, WXGA800-7in
     ABIs : armeabi-v7a, mips, x86
----------
id: 22 or "Google Inc.:Google APIs:17"
     Name: Google APIs
     Type: Add-On
     Vendor: Google Inc.
     Revision: 1
     Description: Android + Google APIs
     Based on Android 4.2 (API level 17)
     Libraries:
      * com.google.android.media.effects (effects.jar)
          Collection of video effects
      * com.android.future.usb.accessory (usb.jar)
          API for USB Accessories
      * com.google.android.maps (maps.jar)
          API for Google Maps
     Skins: WVGA854, WQVGA400, WSVGA, WXGA800-7in, WXGA720, HVGA, WQVGA432, WVGA800 (default), QVGA, WXGA800
     ABIs : armeabi-v7a
input target id:
3


마지막으로 프로젝트명을 입력하면 프로젝트가 생성됩니다.

input your project name:
cocos2dxTest
Created project directory: /users/coz/desktop/cocos2d-x/cocos2dxTest/proj.android
Created directory /Users/coz/Desktop/cocos2d-x/cocos2dTest/proj.android/src/com/ohjung/cocos2dxtest
Added file /users/coz/desktop/cocos2d-x/cocos2dTest/proj.android/src/com/ohjung/cocos2dxtest/cocos2dTest.java
Created directory /Users/coz/Desktop/cocos2d-x/cocos2dxTest/proj.android/res
Created directory /Users/coz/Desktop/cocos2d-x/cocos2dxTest/proj.android/bin
Created directory /Users/coz/Desktop/cocos2d-x/cocos2dxTest/proj.android/libs
Created directory /Users/coz/Desktop/cocos2d-x/cocos2dxTest/proj.android/res/values
Added file /users/coz/desktop/cocos2d-x/cocos2dxTest/proj.android/res/values/strings.xml
Created directory /Users/coz/Desktop/cocos2d-x/cocos2dxTest/proj.android/res/layout
Added file /users/coz/desktop/cocos2d-x/cocos2dxTest/proj.android/res/layout/main.xml
Added file /users/coz/desktop/cocos2d-x/cocos2dxTest/proj.android/AndroidManifest.xml
Added file /users/coz/desktop/cocos2d-x/cocos2dxTest/proj.android/build.xml
Added file /users/coz/desktop/cocos2d-x/cocos2dxTest/proj.android/proguard-project.txt
Resolved location of library project to: /Users/coz/Desktop/cocos2d-x/cocos2dx/platform/android/java
Updated project.properties
Updated local.properties
Updated file /users/coz/desktop/cocos2d-x/cocos2dxTest/proj.android/proguard-project.txt


(만들어진 폴더모습. android폴더는 cocos2d-x 버전 2.0부터 proj.android로 바뀌었습니다. 스샷은 예전 것으로 그냥 쓰겠습니다)



cocos2dx 설치폴더에 proj.android, Classes, Resources 세개의 폴더를 가지는 프로젝트폴더가 만들어졌습니다.
프로젝트를 만들고 소스코드들은 Classes폴더에 위치하도록 하고, 이미지등의 리소스들은 Resources폴더에.
마지막으로 프로젝트를 빌드하려면 android 폴더의 build_native.sh 를 실행합니다.



5. 안드로이드 프로젝트 빌드 및 실행

터미널로 안드로이드 프로젝트 proj.android 폴더에서 (스샷의 android 폴더)

Kwon-ui-MacBook-Pro:proj.android coz$ ./build_native.sh  <= 빌드 쉘스크립트 실행

//이전 버전 로그입니다.
Using prebuilt externals
make: Entering directory `/Users/coz/Desktop/cocos2d-2/cocos2dxTest/android'
Compile++ thumb  : game_shared <= main.cpp
Compile++ thumb  : game_shared <= AppDelegate.cpp
Compile++ thumb  : game_shared <= HelloWorldScene.cpp
Prebuilt       : libgnustl_static.a <= <NDK>/sources/cxx-stl/gnu-libstdc++/libs/armeabi/
.
.
.
SharedLibrary  : libgame.so
Install        : libgame.so => libs/armeabi/libgame.so
make: Leaving directory `/Users/coz/Desktop/cocos2d-2/cocos2dxTest/android'
//이전 버전 로그입니다.
쭉 컴파일이 되고, 완료가 되면 이클립스에서 프로젝트를 생성합니다.


6. 이클립스에서 프로젝트 생성

(이전 버전 스샷입니다. )


File - New - AndroidProject에서 exist project로 cocos2dx 설치경로에 만들어뒀던 프로젝트의 android폴더를 지정합니다.
우리는 desktop/cocos2d-x/cocos2dxTest/proj.android 폴더를 열면 되겠죠.
(이클립스 JUNO에서는 File-New-Android Project From Existing Code가 있네요)

(저는 이미 만들어서 error메시지가 뜬 상황입니다.)

빌드해서 실행해보시면 정상적으로 되실겁니다.

12.10.31 : 2.0.3부터는 이클립스에서 프로젝트 만들어도 바로 실행되지 않습니다.
cocos2d-x lib가 외부 프로젝트로 되어있어서 임포트를 해야하는데요.
이클립스 File->Import에서 General-Existing Projects into Workspace로 다음의 프로젝트 경로를 임포트합니다.
cocos2d-x(cocos2d-x설치폴더) / cocos2dx / platform / android / java
임포트 후 이클립스 workspace에는 libcocos2dx 프로젝트가 생성되고, 정상적으로 실행이 가능합니다.


완성공!

주의 : R.java파일이 만들어지지 않거나 기타 에러들이 뜨는 상황.
res->drawable에 아이콘 이미지가 icon.png 가 아닌 다른 이름으로 되있는지 확인 (실제로 다른이름으로 생성됨)
Project -> Property -> java build path의 Order and Export 에서 Android 4.0.3등의 해당되는 라이브러리를 가장 위로 올림.
같은 프로퍼티의 java compiler -> compiler level이 1.6 인지 확인.
문제가 없는 것 같은데 계속 에러가 사라지지 않으면, 프로젝트 삭제 후 다시 생성.

12.7.19 : 2.0에서의 문제로 보이며, 2.0.1 버전에서 해결됨




7. xcode 프로젝트 생성

위에서 만든 cocos2dx프로젝트를 생성합니다.

이 때, android 와 iOS개발을 동시에 같은 프로젝트 폴더에서 진행하려면, 
아까 안드로이드 프로젝트 만들 때 지정했던 것과 같은 이름으로 작성합니다. 
cocos2dxTest였지요. (패키지명이 아니고 프로젝트 이름)
대신, desktop/cocos2d-x/안에 생성하려하면 안되겠죠. 이미 안드로이드용 프로젝트 폴더가 있을테니까요.

적당히 임시로 폴더를 지정하시고 Create !

완성공 !

8. android, iOS 같은 프로젝트 폴더에서 작업하기  (2012.09.05 수정되었습니다.)

8-1.  7번에서 만든 아이폰 프로젝트 폴더의 하위폴더들을 (cocos2dxTest의 하위폴더) 
        5번에서 만든 안드로이드 프로젝트 폴더(cocos2dxTest폴더) 아래로 복사합니다. (cocos2dxTest/android 가 아닙니다)
아이폰 프로젝트의 ios, libs 폴더를 복사해서 안드로이드 프로젝트로 넣으면 되겠죠
(Resources 폴더안에 있는 이미지들도 복사해주세요.)

8-2 . 7번에서 만든 아이폰 프로젝트의 프로젝트 파일(cocos2dxTest.xcodeproj)을 cocos2dx 설치 폴더로 복사합니다.
        (cocos2d 2.0부터 xcodeproj파일을 프로젝트 폴더에 넣습니다. cocos2dx-2/cocos2dxTest/cocos2dxTest.xcodeproj )
cocos2dxTest.xcodeproj 파일을 안드로이드 동일한 폴더로 복사.


젤 윗 스샷이 이번에 만든 iOS 프로젝트 폴더
두번째 스샷이 하위폴더들과 프로젝트 파일을 복사해서 cocos2d 설치폴더에 복사한 후
세번째는 프로젝트 폴더 최종 모습.

안드로이드와 아이폰 개발을 하나의 폴더에서 진행할 수 있게되었습니다.


9. 멀티플랫폼 개발 테스트.

xcode 프로젝트에서 간단하게 Hello World라는 텍스트를 수정하여 테스트.



아이폰 시뮬레이터 실행
이제 터미널에서 안드로이드 프로젝트 컴파일.
OhJung-Kwon-ui-MacBook:android coz$ ./build_native.sh
Using prebuilt externals
make: Entering directory `/Users/coz/Desktop/cocos2d-2/cocos2dxTest/android'
Compile++ thumb  : game_shared <= main.cpp

컴파일 완료 후
이클립스 프로젝트 Refresh 한번 해주고 실행!

완성공!!!

[2012.09.12 수정]
본 게시글은 cocos2d-1.0.1x-0.13버전에서 정리된 것입니다.
2.0으로 업데이트 되면서, 수정된 부분이 보이면 고치고 있는데요.

2.0에서 부터 안드로이드 시뮬레이터에서는 스샷의 저런 화면이 보이지 않고
액티비티만 출력됐다 내려가는 것으로 확인됐는데,
cocos2d-x 2.0부터 opengl es 2.0 이상만 지원하도록 되어 그런것으로 추정 하고 있습니다.

안드로이드 폰에 올려 확인해보시면 정상구동 보실 수 있습니다.






즐코딩하시기바랍니다


액션바 taps+swipe에서 잠시 언급했던 fragment에 대해 이번엔 정리해 보도록 하겠습니다.

안드로이드 플랫폼 3.0 버전인 허니컴Honeycomb부터 큰 화면의 태블릿을 위해 도입된 대표적인 컴포넌트가 프래그먼트(Fragment)와 액션바(ActionBar)입니다.그 중 액션바는 이전 게시판에서 이야기했으니 오늘은 프래그먼트에 대해서입니다.

 프래그먼트 개요

만약 스마트 폰 앱을 개발할 때 세로모드(Portrait)에서는 단순하게 화면을 구성하지만 가로모드(Landscape) 화면 구성을 좀더 다양한 화면으로 구성 하고 싶다면 어떻게 할지 고민할 수 있습니다. 또는 해상도나 디바이스의 스크린크기, 디바이스 종류에 따라서 보여지는 것들을 다르게 구성하고 싶어할 수도 있습니다.

안드로이드에서는 이러한 요구사항을 만족시켜줄 수 있는 것을 fragment라는 개념으로 추가하게 되었습니다.

안드로이드 3.0이전 버전에서는 한 화면에 보이는 모든 것을 관장하는 것이 Activity라는 개념이였는데 이러한 Activity는 하나의 화면에 여러개 사용할 수 없게 설계가 되어있습니다.

그러나 하나의 액티비티로 상호작용하는 것은 큰 화면의 기기에서 비효율적이며 자원 낭비가 심하고 또한 일부 화면을 위한 자원을 재사용할 수 없고, 다양한 내용을 표시하려면 구성이 매우 복잡했습니다.

그래서 하나의 화면에 Activity와 비슷한 개념을 가지면서도 여러가지 화면을 넣을 수 있는 방법으로 fragment가 등장하게 되었습니다. 

 프래그먼트 특징

1) 프래그먼트는 액티비티 조각 혹은 서브액티비티(subactivity)

2) 하나의 액티비티에서 다수의 프래그먼트를 결합하여 다중 패널multi-pane UI를 생성할 수 있음

3) 여러 액티비티에서 하나의 프래그먼트를 재활용 가능

4) 프래그먼트 자신만의 생명주기 가짐

5) 입력 이벤트를 받아들이며, 액티비티가 실행하는 동안 동적으로 추가 혹은 삭제 가능

6) activity 안에서만 존재할 수 있고 단독으로 존재할 수 없다.

7) activity 안에서 다른 view와 함께 존재 가능

8) back stack을 사용가능

9) 반드시 default 생성자 존재

 프래그먼트 디자인 철학

애플리케이션을 태블릿과 핸드셋에 모두 지원하도록 설계하고자 한다면 화면의 사이즈에 따라 최적의 UX을 맛볼 수 있도록 프래그먼트를 다양한 레이아웃 구성에 재사용할 수 있습니다.

작은 화면사이즈 기기에는 하나의 액티비티에 하나의 프래그먼트를 사용하는 1-패널 UI를 제공하고, 태블릿과 같은 큰 화면사이즈 기기에서는 2개의 프래그먼트를 하나의 액티비티에 조합하여 2-패널UI를 제공하면 됩니다. 

 프래그먼트 생명주기

● 활성resumed 상태 : 실행 중인 액티비티에서 프래그먼트가 보이는 상태

● 중지paused 상태 : 다른 액티비티가 포그라운드 상태이며 포커스를 가지고 있지만 프래그먼트가 거주하는 액티비티가 여전히 보이는 상태

● 정지stopped 상태 : 프래그먼트가 보이지 않는다. 호스트 액티비티가 정지되거나 프래그먼트가 액티비티에서 제거되고 백스택에 추가되어 있는 상태 정지된 프래그먼트는 여전히 살아 있어 모든 상태와 멤버 정보가 시스템에 보관되어 있으나 사용자에게 더 이상 보이지 않고 액티비티가 종료되면 프래그먼트도 같이 종료됨

아래 소스에서 사용한 메소드는 onActivityCreated,onResume,onPause, onCreateView정도여서 http://blog.saltfactory.net/190 의 설명을 인용하도록 하겠습니다.

● onAttach()

onAttach() 콜백 메소드는 fargment가 activity에 추가되고 나면 호출된다. 이때 Activity가 파라미터로 전달되게 된다. Fragment는 Activity가 아니다. 다시말해서 Activity는 Context를 상속받아서 Context가 가지고 있는 많이 있지만 Fragment는 android.app 의 Object 클래스를 상속받아서 만들어진 클래스이다. 그래서 Fragment에서는 Context의 기능을 바로 사용할 수 없다. 뿐만 아니라 Fragment는  Activity 안에서만 존재하기 때문에 Activity를 onAttach() 콜백으로 받아올 수 있다.

1
2
3
public void onAttach(Activity activity) {
     super.onAttach(activity);
}


● onCreate()

프래그먼트가 생성될 때 호출된다. 프래그먼트가 중지 혹은 정지된 후 재개될 때 보유하기 원하는 프래그먼트의 필수 컴포넌트을 초기화한다.

Fragment의 onCreate() 메소드는 Activity의 onCreate() 메소드와 비슷하지만 Bundle을 받아오기 때문에 bundle에 대한 속성을 사용할 수 있다.

1
2
3
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
}


● onCreateView()

Fragment의 onCreateView()는 Fragment에 실제 사용할 뷰를 만드는 작업을 하는 메소드이다. LayoutInflater를 인자로 받아서 layout으로 설정한 XML을 연결하거나 bundle에 의한 작업을 하는 메소드이다. 

1
2
3
4
5
6
public View onCreateView(LayoutInflater inflater, ViewGroup container,
                Bundle savedInstanceState)
{
        view = inflater.inflate(R.layout.table, container, false);
        return view;
}


● onActivityCreated()

onActivityCreate() 메소드는 Activity에서 Fragment를 모두 생성하고 난 다음에 (Activity의 onCreate()가 마치고 난 다음)에 생성되는 메소드이다. 이 메소드가 호출될 때에서는 Activity의 모든 View가 만들어지고 난 다음이기 때문에 View를 변경하는 등의 작업을 할 수 있다.그러나, fragment에서 생성된 view의 size를 알려고 하니 이 메소드에서는 getWidth가 0으로 나와 따로 메소

1
2
3
4
public void onActivityCreated(Bundle savedInstanceState) {
    //Log.d(TAG,"onActivityCreated");
    super.onActivityCreated(savedInstanceState);
}


● onStart()

이 메소드가 호출되면 화면의 모든 UI가 만들어진 지고 호출이 된다.

1
2
3
public void onStart(){
    super.onStart();
}


● onResume()

이 메소드가 호출되고 난 다음에 사용자와 Fragment와 상호작용이 가능하다. 다시 말해서 이 곳에서 사용자가 버튼을 누르거나 하는 이벤트를 받을 수 있게 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public void onResume() {
    super.onResume();
 
    ⁄⁄mStatus.setText("현재 상태 : 서비스 시작");
    cellwidth = new int[5];
    desity = this.getResources().getDisplayMetrics().density;
    appendHeader();
    appendRowBlank();
    datainfo = selectData();
    appendRow(datainfo);   
    ViewTreeObserver vto = headerView[0].getViewTreeObserver();
    vto.addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
      @SuppressWarnings("deprecation")
    @Override
      public void onGlobalLayout() {
        ⁄⁄Log.d(TAG, "Height = " + headerView[0].getWidth() + " Width = " + tv[0][0].getWidth());
        ViewTreeObserver obs = headerView[0].getViewTreeObserver();
        resizeRow();
        obs.removeGlobalOnLayoutListener(this);
      }
    });    
     
}  


● onPause()

이 메소드는 Fragment가 다시 돌아갈 때 (Back) 처음으로 불려지는 콜백 메소드이다. 

1
2
3
4
@Override
 public void onPause() {
     super.onPause();
 }


● onSaveInstanceState()

이 메소드에서는 Activity와 동일하게 Fragment가 사라질때 현재의 상태를 저장하고 나중에 Fragment가 돌아오면 다시 저장한 내용을 사용할 수 있게해주는 메소드이다.

1
2
3
4
5
@Override
public void onSaveInstanceState(Bundle savedInstanceState) {
    super.onSaveInstanceState(savedInstanceState);
    savedInstanceState.putInt("text", "savedText");
}


● onStop()

Fragment의 onStop() 메소드는 Activity의 onStop()메소드와 비슷하다. 이 콜백 메소드가 호출되면 Fragment가 더이상 보이지 않는 상태이고 더이상 Activity에서 Fragment에게 오퍼레이션을 할 수 없게 된다.

1
2
3
4
@Override
public void onStop() {
    super.onStop();
}


 onDestroyView()

Fragment의 View가 모두 소멸될 때 호출되는 콜백 메소드이다. 이때 View에 관련된 모든 자원들이 사라지게 된다.

1
2
3
4
@Override
public void onDestroyView() {
    super.onDestroyView();
}


● onDestroy()

Fragment를 더이상 사용하지 않을 때 호출되는 콜백 메소드이다.  하지만 Activity와의 연결은 아직 끊어진 상태는 아니다.

1
2
3
4
@Override
public void onDestroy() {
    super.onDestroy();
}


● onDetach()

Fragment가 더이상 Activity와 관계가 없을 때 두 사이의 연결을 끊으며 Fragment에 관련된 모든 자원들이 사라지게 된다.

1
2
3
4
5
@Override
 public void onDetach() {
    super.onDetach();
    activity = null;
 }

 전체소스

 PublicWifi_0409.zip

참고사이트

http://developer.android.com/guide/components/fragments.html

http://blog.saltfactory.net/190



출처 : http://tigerwoods.tistory.com/31


예제 프로젝트 다운로드



 

  

1. 환경설정 개요 (Preferences)

 

안드로이드 플랫폼은 Data를 저장하는 방법으로 환경설정(이하 Preferences), 파일, Local DB, 네트워크를 제공한다.

 

그 중 Preferences는 가장 간단하게 정보를 저장하는 방법(mechanism)을 제공하며, App이나 그 컴포넌트 (Activity, Service 등)의 환경 설정 정보를 저장/복원하는 용도로 사용된다.

 

 

▌Preferences의 형태▐

안드로이드에서 Preferences는 ListView의 형태로 표현되며 쉬운 Preferences의 구현을 위해 PreferenceActivity 클래스를 제공한다. PreferenceActivity 클래스는 XML 기반의 Preference 정의 문서를 통해 App 파일 저장소에 Preferences 파일을 생성하고 사용하는 방식으로 작동한다. (참고: 일반 Activity를 사용해 ListView형태의 Preference Activity가 아닌 커스텀 Preference Activity를 구현 할 수도 있지만 (Container + 각종 UI 위젯 사용) 이 글에서는 그 방법에 대해 다루지 않으려고 합니다)

 

 

▌Preferences 데이터 저장▐

Preferences를 위한 데이터 저장 시 사용되는 자료구조는 Map 방식이다. 즉 키(key)-값(value) 한 쌍으로 이루어진 1~n개의 항목으로 구성된다. 키(key) 는 String 타입으로, 말 그대로 각 데이터에 접근할 수 있게 하는 유일한 구분자며, 값(Value)은 기본 자료형 (int, boolean, string 등) 기반의 데이터로 구성된다. 다음은 Map 데이터 구조의 한가지 예이다.

 키(Key) - String 형
 값(Value) - 기본자료형
 Font
 "Noraml"
 FontSize 20
 FontColor FFFFFFFF
 ... ...
 ... ...


 

위와 같은 Preferences데이터는 안드로이드 디바이스의

data/data/App 패키지 이름/shared_prefs/

경로에 XML형태의 파일로 생성된다. xml 파일 이름은, 파일 생성/접근을 위해 어떤 메서드를 사용하느냐에 따라 시스템이 지정 할 수도 있고, 개발자가 임의의 파일 이름을 지정할 수도 있다.

 

 

참고로, 안드로이드에서 Preferences 데이터 파일을 다수의 App간에 공유하는 것은 기본적으로 불가능 하게 디자인 되어 있다. (참고: 여기서 불가능하다는 뜻은 Preferences Framework에서는 타 App의 환경 설정 파일에 접근할 수 있는 메서드를 제공하지 않는다는 뜻이다. 그럴 필요가 있을지 모르겠지만, 굳이 접근해야 한다면 Preferences 파일 생성 모드를 MODE_WORLD_READABLE로 설정해 guest가 파일을 쓰고 읽을 수 있도록 하고(위에서 MyCustomePref.xml 처럼) Preferences 데이터 파일을 파일 스트림을 통해 읽어와 직접 파싱하고 사용할 수 있는 편법이 있긴 하다)

 

XML파일을 디바이스로부터 추출해 열어보면 Preference 데이터가 다음과 같은 형식으로 저장되어 있음을 볼 수 있다.

1<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
2<map>
3    <boolean name="GreetingMsg" value="true" />
4    <boolean name="sub_checkbox" value="true" />
5    <string name="AdditionalMsg">tigerwoods.tistory.com</string>
6    <string name="TextColor">FF00FF00</string>
7    <string name="Ringtone">content://settings/system/ringtone</string>
8</map>

 

 

Preferences 구현 시 위와 같은 XML 기반 Preferences 데이터 파일을 File I/O API를 사용해 직접 생성/사용 하는 것은 아니다. 안드로이드는 Preferences구현/운용을 위해 android.preference 패키지 및 몇 가지 유용한 클래스/인터페이스(PreferenceActivity, SharedPreferences 등)를 제공하는데, 그것들의 도움을 받으면 손쉽게 Preferences를 구현/운용 할 수 있다.

 

 

▌Preferences 구현▐

안드로이드에서는 Preference 구현을 위해 많은 관련 클래스를 제공하기 때문에 그 클래스들을 적절히 이용하여 다음과 같이 몇 가지 단계만 추가하기만 하면 쉽게 프로젝트에 Preferences 기능을 추가 할 수 있다.

  • 프로젝트 내 어떤 환경정보를 Preferences로 관리 할지 설계
  • 위 설계에 따라 Preferences XML 문서를 작성하고 프로젝트 리소스로 공급 (\res\xml\ 밑)
  • 작성된 Preferences XML문서를 사용하는 PreferenceActivity 구현
  • Preferences가 필요한 Activity로부터 PreferenceActivity를 호출 (Intent 이용)
  • App이 다시 실행될 때 이미 저장된 Preference데이터 파일이 있다면 onResume()과 같은 callback내부에서 환경 설정 값 설정을 불러와 사용

 

위에 나열된 사항 이외에 Preference 데이터 파일의 생성, 데이터 변경 이벤트 listener, 데이터의 저장과 같은 부분은 PreferenceActivity 클래스가 자동으로 관리해 줌으로 개발자는 별로 신경 쓸 필요가 없다. (참고: PreferenceActivity를 사용하지 않고 일반 Activity로 구현한다면 안드로이드가 제공하는 여러 Preferences 관련 클래스 등을 이용해 파일 생성, 이벤트 listener, 저장 등을 직접 구현 해야 한다.)

 

그럼 위 Preference 구현 5단계 중 두 번째인 Preference XML 문서 작성부터 한 번 살펴보자.

 

 

 

 

2. Preference XML 문서 작성하기

 

Preference XML 문서를 작성하기 전에 문서를 구성할 Preferences 관련 클래스들에 관해 약간의 지식이 필요함. 이 글 끝 부분에 "참고. Preference XML 문서 작성시 사용되는 클래스" 단락에 간단한 클래스 설명과 중요 XML 속성, 메서드 등이 설명 되어 있음으로 참고.

 

Preferences XML 문서는 프로젝트 폴더\res\xml\파일이름.xml의 위치에 생성 한다. xml 파일의 이름은 개발자 임의로 지정할 수 있으며 추 후 code 상에서 R.xml.파일이름으로 접근 할 수 있다.

 

이렇게 생성된 XML 문서 내부를 여러 Preference 관련 클래스를 사용해 정의 해야 하는데, 이 글에 포함된 예제 프로젝트에서 사용된 Preference XML 문서를 살펴보면 다음과 같다.

소스 펼치기

 

 

그럼 settings.xml에서 사용된 요소들을 하나씩 분석해 보자

 

 

<PreferenceScreen>

Preferences XML 문서의 root 요소는 항상 <PreferenceScreen>이며 root 요소 내부에는 반드시 xmlns 속성 (xmlns:android=http://......)을 정의해 주어야 한다.

 

root로 사용된 <PreferenceScreen>는 child를 포함하는 container 역할을 하며, PreferenceActivity에게 Preferences 계층 구조를 전달하는 역할을 한다. root로 사용된 <PreferenceScreen>은 container역할 만 하고 자기 자신은 화면에 직접 표현이 안됨으로 title, summary 속성을 지정할 필요가 없다.

 

key 속성에 지정된 문자열은 추 후 code에서 이 preference 계층 구조 내부의 특정 요소를 찾을 수 있는 키 값이 된다.


Preferencescreen root = (PreferenceScreen)getPreference("preference_root");

CheckBoxPreference cb = (CheckBoxPreference)getPreference("checkbox");

 

 

<CheckBoxPreference> & <EditTextPreference>

Root <PreferenceScreen> 밑에 처음 나오는 두 개의 child이며, XML에 추가된 순서대로 Preference view에 표시된다.

각 아이템은 key, title, summary 속성이 지정되어 있으며, checkbox의 경우 초기 설정이 check 상태로 지정되어 있다.

<EditTextPreference> 같은 경우 DialogPreference로부터 상속하며, 클릭 시 별도의 다이얼로그 창을 popup 한다.

 

 

<PreferenceCategory>

예제는 총 2개의 <PreferenceCategory>가 있으며 첫 번째는 <ListPreference>와 <RingtonePreference>를 하나의 Category로 묶는다. 예제 XML의 34번째 라인에 보면 두 번째 Category도 지정되어 있으며 <PreferenceScreen>을 Catefory 멤버로 가지고 있다.

 

<PreferenceCategory>에서 title속성은 다른 Preference 객체와 같이 제목을 표시하지만, summary는 지정되어도 화면에 표시 되지 않는다.

 

또, 첫 번째 category의 속성 중 android:enabled 속성이 false로 지정되어 있음으로 Preference초기 실행 시 child인 <ListPreference>와 <RingtonePreference>는 비활성화 상태로 표현된다. 이와 같이 여러 Preference 아이템을 하나의 category로 묶으면 그룹 속성을 지정하는 것이 가능하다. (Preference의 마지막 항목, checkbox를 check하면 첫 번째 category를 활성화 함)

 

 

<ListPreference>

다른 항목들과 마찬가지로 key, title, summary가 설정되어 있으며, entries 속성에는 사용자에게 보일 ListView 아이템을 지정하며, entryValues속성에는 컴퓨터가 처리할 처리 할 정보를 제공한다. 예를 들면 entries – "Red", "Green", "Blue" 등 사람이 읽을 수 있는 값을 제공하고, entryValues에는 "FFFF0000", "FF00FF00", "FF0000FF" 등 컴퓨터가 사용할 정보를 제공한다.

 

또, android:defaultValue 속성이 "FFFFFFFF"으로 지정되어 있어 사용자가 설정 값을 바꾸기 전까지는 "FFFFFFFF" (흰색)이 기본 값으로 사용된다.

 

 

<RingtonePreference>

key, title, summary가 지정되어 있으며, showDefault와 showSilent가 true로 설정되어 Default 항목과 Slient 항목이 리스트에 표시된다. (<RingtonePreference>는 사용자가 지정한 정보를 바탕으로 Ringtone 설정 기능을 제공하는 Activity들을 찾는 Intent를 발생 시킨다. 디바이스에 여러 종류의 ringtone과 ringtone 관련 activity를 제공하는 app이 설치되어 있다면 더 많은 ringtone 옵션이 화면에 표시 될 수도 있다)

 

 

<PreferenceCategory>

두 번째 그룹으로 CheckBox를 별도의 화면에 표현하는 <PreferenceScreen> child를 포함.

 

 

<PreferenceScreen>

<PreferenceScreen>가 child 요소로 사용된 경우. Preferences에서 하나의 아이템으로 표현되기 때문에 title, summary가 지정되어 있다. 이 아이템을 선택하면 별도의 Preference 창 (dialog 기반)이 Popup한다.

 

 

<CheckBoxPreference>

key, title이 지정되어 있으며, summayOn과 summaryOff에는 각각 check상태의 도움말과 uncheck상태의 도움말이 설정 되어 있다.

 

 

 

 

3. XML + PreferenceActivity이용해 Preference 구현하기

 

작성된 Preference XML 파일을 화면에 표현 가능한 Preference로 만드는데 핵심적인 역할을 하는 것이 바로 PreferenceActivity이다. 전에 살펴본 적이 있는 ListActivity, TabActivity와 마찬가지로 Activity로 상속하며, Preference를 화면에 표현하고 운영하는데 특화된 클래스이다.

 

PreferenceActivity는 Preference 생성/운영에 도움이 되는 다음과 같은 편리한 기능이 구현되어 있다.

  • XML부터 Preference 계층 구조를 전달 받는 메서드 제공
  • 타 Activity의 Preference 계층 구조를 받아오는 메서드 제공
  • 제공된 Preference 계층 구조를 ListView 형태로 자동 구성/표현
  • 제공된 Preference 계층 구조에 따라 디바이스에 Preference 데이터 파일 자동 생성
  • 사용자가 변경한 사항들을 Preference 데이터 파일에 자동 저장

 

 

PreferenceActivity를 이용해 Preference를 구현하려면 크게 두 가지 단계만 거치면 되는데 다음과 같다. (물론 PreferenceActivity도 Activity 생명 주기를 따르기 때문에 onPause(), onResume() 메서드 같은 생명 주기 관련 callback의 구현도 신경 써야 하지만 여기서는 Preference를 구현하는 최소한의 기능만 설명한다)

  • PreferenceActivity를 상속하는 커스텀 클래스 정의.
  • onCreate()에 내부에서 Preference 계층 구조를 가져오는 메서드 호출.

 

위에서 두 번째 구현 순서로 설명된 Preference 계층 구조를 가져오는 메서드는 2개가 제공된다.

우선, \res\xml 밑의 Preference XML로부터 Preference 계층 구조를 얻어올 수 있는 메서드는 다음과 같다.

void addPreferencesFromResource(int)

Parameter:

  • int: Preference XML Resource. (예. R.xml.preferences)

 

두 번째로 타 Activity가 사용하는 Preference 계층 구조를 가져와 적용 시킬 수도 있는데, 이 때 사용되는 메서드는 다음과 같다.

(이 부분은 해결이 안 되는 중요한 버그가 있습니다. main preference까지는 잘 가져와 지는데 별도의 dialog를 띄우는 Preference 아이템을 클릭하면 WindowsManager가 BadTokenException을 발생합니다. 자세한 증상은 첨부된 예제 프로젝트에서 확인하시고, 혹시 해결 방법을 아시는 분은 꼭 댓글 부탁 드립니다)

void addPreferencesFromIntent(Intent)

Parameter:

  • Intent: Preference 계층 구조를 제공할 Activity를 지정하는 Intent

 

구현이 완료된 PreferenceActivity는 타 Activity로 부터 호출 되면 ListView 형태의 Preference 화면을 사용자에게 제공하고, 사용자가 정보를 수정하면 자동으로 Preference 데이터에 저장 하게 된다.

 

PreferenceActivity가 많은 편의 기능을 제공하지만 저장된 설정 복원은 개발자의 몫이다. 즉, 포함된 App이 다시 실행될 때, 기존에 저장된 Preferences설정 값을 Preference 데이터 파일로부터 읽어와 App에 적용 시키는 작업 (ex. App시작 시 Preference 파일에 저장된 폰트 색깔 등을 로드 해 App에 적용시키는)은 개발자가 직접 구현해 주어야 한다. 이를 위해 다음 단락에는 저장된 Preference 데이터 파일에 접근하여 저장된 정보를 가져오는 방법에 대해 알아본다.

 

 

 

 

4. Preference 데이터 파일로부터 환경 복원하기

 

안드로이드에서는 SharedPreferences 인터페이스를 이용해 저장된 Preferences 데이터 파일에 접근한다.

 

 

▌SharedPreferences 인터페이스 얻기▐

 

안드로이드 코드 내에서 SharedPreferences 인터페이스를 얻는 방법은 다음 나열된 세 개의 메서드를 통해서 가능하다. 



SharedPreferences Activity.getPreferences(int)

Parameter:

  • int: Preference 데이터 파일의 생성 시 접근 권한을 설정. Context클래스에 상수로 선언된 MODE_PRIVATE, MODE_WORLD_READABLE, MODE_WORLD_WRITABLE을 설정 가능. MODE_PRIVATE는 지금 App 만 접근 가능하도록 파일을 생성하며 (_rw_rw____), MODE_WORLD_READABLE/WRITABLE은 타 App에서도 파일을 읽고/쓸 수 있도록 생성한다 (_rw_rw_r__ 또는 _rw_rw__w_ 또는 |를 이용해 두 속성을 모두 지정하면 _rw_rw_rw_)

Return:

  • SharedPreferences: 메서드가 호출되는 Activity 클래스의 이름으로 저장된 Preference 데이터 파일의 SharedPreferences 인터페이스를 리턴. 같은 이름의 Preference 데이터 파일이 없을 경우 새로 생성

 

예를 들면 Activity를 상속하는 A, B라는 클래스 이름의 커스텀 Activity가 정의 되어 있다. A 내부에서 getPreference를 호출할 경우 디바이스의 Preference 저장 위치(data/data/패키지이름/shared_prefs/)에 A.xml 이라는 Preference 데이터 파일을 검색하고 파일이 존재 한다면 해당 파일에 대한 SharedPreferences 인터페이스를 리턴 하며, 파일이 존재하지 않을 경우에는 A.xml 파일을 생성한 후 생성한 파일에 대한 SharedPreferences 인터페이스를 리턴한다. 마찬가지로, B 내부에서 getPreference를 호출할 경우 디바이스에서 B.xml 파일을 검색 후 해당 파일에 대한 SharedPreferences 인터페이스를 리턴 한다 (없으면 파일 생성 후 인터페이스 리턴).


 

SharedPreferences Context.getSharedPreferences(String, int)

Parameter:

  • String: 사용(생성)할 Preference 데이터 파일의 이름을 지정
  • int: 생성될 Preference 데이터 파일의 생성 시 접근 권한을 설정

Return:

  • SharedPreferences: 첫 번째 인자로 전달되는 문자열과 일치하는 이름의 Preference 데이터 파일의 SharedPreferences 인터페이스를 리턴. 만약 같은 이름의 파일이 없다면 파일을 새로 생성하고 생성된 파일에 대한 SharedPreferences 인터페이스를 리턴

 

한가지 참고할 사항은 이 메서드를 통해 개발자가 생성/사용될 Preference 데이터 파일의 이름을 임의로 설정할 수 있다는 것인데, 예를 들어, A라는 Activity에서 getSharedPreferences("initial_setting", MODE_PRIVATE)라는 메서드를 실행한다면, preference framework는 /data/data/패키지/shared_pref/ 밑에서 initial_setting.xml이라는 파일을 검색하고 없다면 이를 새로 생성 후 initial_setting.xml에 대한 SharedPreferences 인스턴스를 리턴 하게 된다..

 

제일 처음에 설명한 Activity.getPreferences(int)도 실제로는 자기자신을 호출한 Activity(또는 컴포넌트)의 이름과 입력된 int 형 인자를 가지고 본 메서드를 호출하는 형태로 구현되어 있다.




Static SharedPreferences PreferenceManager.getDefaultSharedPreferences(Context)

Parameter:

  • Context: 사용하기 원하는 SharedPreferences 인터페이스를 포함하는 Context 지정

Return:

  • SharedPreferences: 인자로 지정된 Context (app 구동 환경)가 기본으로 생성하는 Preference 데이터 파일에 대한 SharedPreferences를 리턴

 

이 메서드는 App level의 기본 Preference 데이터 파일을 생성/사용하는데 사용된다. 예를 들어 com.holim.test.aaa 라는 패키지에 포함되는 A Activity에서 본 메소드를 호출 하면 Preference 프레임웍은 /data/data/패키지이름/shared_pref/com.holim.test.aaa_preferences.xml 이라는 Preference 데이터 파일을 생성한다. 전달된 Context 인자에 따라 패키지이름_preferences.xml 형태로 생성/사용할 Preference 데이터 파일의 이름이 시스템에 의해 자동으로 지정되게 되기 때문에, 호출되는 위치가 어디이든 같은 Context를 인자로 전달해 호출한 getDefaultSharedPreferences(…)메서드는 항상 같은 SharedPreferences 인스턴스를 리턴한다.

 

한가지 참고할 사항은 본 메서드는 static 메서드이기 때문에 메서드를 제공하는 PreferenceManager 클래스를 따로 생성할 필요 없이 다음과 같이 사용하면 된다.

SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(this);

 

 

이와 같이 여러 메서드를 사용해 얻어온 SharedPreferences 인터페이스로 무엇을 할 수 있는 지 알아 보자. 다음은 SharedPreferences 인터페이스가 제공하는 중요 메서드들이다.

 

 

▌SharedPreferences 인터페이스▐

 

SharedPreferences 인터페이스가 제공하는 기능은 다음과 같다.

  • 디바이스에 저장되어 있는 Preference 데이터 파일로부터 데이터를 추출하는 여러 메서드
  • Preference 데이터를 수정하는 방법을 제공하는 Editor 내부 인터페이스
  • Preference 데이터가 변경되었을 때 호출되는 callback 인터페이스

 

중요 메서드

SharedPreferences.Editor edit() – SharedPreferences가 연결된 Preference 데이터 파일을 수정 할 수 있게 여러 메서드를 제공하는 Editor 인터페이스 리턴. Editor 인터페이스는 자료 수정을 완료 한 후 Editor.commit() 메소드를 사용해야 파일에 변경내용이 저장됨.

void registerOnSharedpreferenceChangeListener(…) – Preference 변경 이벤트 Listener 등록

void unregisterOnSharedPreferenceChangeListener(…) – 위에 등록한 이벤트 Listener 등록 해지

Map <String, ?> getAll() – 모든 Preferences 값을 Map 자료형으로 리턴

boolean contains(String) – 인자로 제공되는 문자열과 같은 key의 Preference 항목이 있으면 true 리턴

타입 get타입(String, 타입) – Boolean, Float, Int, Long, String 형 메서드가 있으며 첫 인자는 데이터를 가져올 Key 값이며, 두 번째 인자는 찾는 preference 데이터가 존재하지 않을 때 리턴 할 값이다.

Boolean isMarried = sharedPref.getBoolean("결혼여부", false);

int fontSize = sharedPref.getInt("폰트사이즈", 20);

 

중요 인터페이스

  • SharedPreferences.Editor – Preference 파일에 저장된 항목을 수정/제거 할 수 있는 여러 메서드를 제공하는 인터페이스
  • SharedPreferences.OnSharedPreferenceChangeListener – Preference 데이터가 변경되었을 때 호출되는 callback 인터페이스

 

 

예제 프로젝트 중 MainActivity.java의 onCreate(…)와 onPause(…)메서드에서 보면 지금까지 설명한 SharedPreferences 인터페이스 얻기와 인터페이스가 제공하는 여러 메서드를 사용해 Preference 데이터 파일로부터 저장된 정보를 가져와 사용하는 것을 볼 수 있다.


MainActivity.onCreate()

소스 펼치기

 

MainActivity.onResume() 

소스 펼치기

 

 

 

 

참고. Preference XML 문서 작성시 사용되는 클래스

 

전 글들에서 다루었던 XML을 이용한 UI 및 메뉴 구성에서도 봤듯이

XML을 이용하면 디자인과 Logic을 분리 시킬 수 있어 간결한 코드의 구성이 가능할 뿐 아니라 현지화/유지보수 등 작업을 하는데 훨씬 편리 할 수 있음으로 권장되는 구현 방식 이라고 설명 한 적이 있다.

 

Preference에서도 마찬가지로 XML을 이용해 Preferences 구조를 쉽게 정의할 수 있다.

 

Preference XML 문서 작성시 android.preference 패키지 밑의 여러 클래스가 사용되는데 패키지 내부의 중요한 클래스의 상속 구조는 다음과 같다.

 

 

붉은 색 사각형 내부의 클래스가 Preferences 과 직접적으로 연관이 있는 클래스이며, 그 중 Preference 클래스를 비롯한 하위 클래스들이 XML Preference 문서 작성에 사용된다.

 

그럼 Preference 클래스부터 한번 살펴보자

  


 

▌Preference 클래스▐

여러 Preference 관련 클래스의 최상위 부모 클래스 이므로, 제공하는 XML 속성, 메서드는 자식 Preferences 관련 클래스에서도 모두 사용할 수 있다. XML 내부에서 직접적으로 사용되지는 않는다.

 

중요 XML 속성

  • android:key – Preferences 데이터 저장/불러올 때 사용되는 키(key) 지정
  • android:title – Preference 항목의 제목을 지정
  • android:summary – Preference 항목을 자세히 설명하는 문자열 지정
  • android:enabled – Preference 항목의 활성화 비활성화 여부 지정 (true/false)
  • android:selectable – Preference 항목의 선택 가능 여부 결정 (true/false)
  • android:order – Preference 항목이 표시되는 순서를 결정 (0-based 정수 사용: 낮은 값 먼저 보임). 순서가 명시적으로 지정되지 않는다면 XML에 정의된 순서대로 표시됨

 

중요 메서드

  • void setKey/Title/Summary(…) – Preference 항목의 키/제목/설명을 지정
  • void setEnabled(boolean) - Preference항목의 활성화/비활성화 여부 지정
  • void setSelectable(boolean) – Preference 항목의 선택 가능 여부 결정
  • void setLayoutResource(int) – Preference 항목에 표시할 View를 R 클래스로부터 지정
  • void setOnPreferenceChangedListener(…) – Preference 항목에 변화가 있으면 호출될 callback을 지정
  • void setOnPreferenceClickListener(…) – Preference 항목에 click 이벤트가 발생하면 호출될 callback을 지정

 

중요 인터페이스

  • Preference.OnPreferenceChangeListener - 사용자가 Preference를 변경하였을 때 호출되는 callback에 대한 인트페이스
  • Preference.OnPreferenceClickListener - 사용자가 Preference를 클릭하였을 때 호출되는 callback에 대한 인터페이스

 

  


▌PreferenceGroup 클래스▐

 

Preferences를 구성하는 객체들을 담을 수 있는 Container 클래스. PreferenceCategory와 PreferenceScreeen을 파생. XML 내부에서 직접적으로 사용되지는 않음.

 

중요 메서드

  • boolean addPreference(Preference) – 새로운 Preference 항목을 그룹에 추가. 추가 성공/실패 리턴.
  • Preference findPreference(String) – 인자로 전달되는 문자열과 같은 key값을 갖는 group 내 (자기자신포함) Preference 항목 리턴
  • Preference findPreference(int) – 인자(0-based정수)에 명시된 위치의 Preference 항목 리턴. 기본적으로 가장 처음 추가된 항목이 index 0
  • void removeAll() – 이 그룹 내 모든 Preference 항목을 삭제함
  • void removePreference(Preference) - 인자로 전달된Preference 항목을 삭제함
  • void setEnabled(boolean) – 그룹 전체의 활성화/비활성화 여부 지정
  • void setOrderingAsAdded(boolean) – true일 경우 그룹에 추가된 순서로 표현. false일 경우 Preference아이템 내 순서 정보가 있으면 그걸 따르고 순서 정보가 없으면 Preference 아이템의 title을 이용해 알파벳순 정렬.

  


 

▌PreferenceScreen 클래스▐

 

PreferenceGroup에서 파생하며, 실제로 Preference XML 문서 내부에 사용 되는 클래스이다. 여러 Preference 구성 요소를 담을 수 있는 root container역할을 하며 Activity에 표현될 Preference의 계층구조를 나타내는 클래스이다. PreferenceActivity에 전달되어 이 클래스의 인스턴스가 포함하는 것들을 화면에 표현한다.

 

이 클래스가 Preference XML 문서에서 사용될 때는 다음과 같이 두 가지 용도로 사용된다.

  • Preference XML 문서의 root 요소 사용 시 - 경우에는 PreferenceActivity에 전달되어 preference의 전체 구조를 전달하는 용도로 사용. 이 경우 자기 자신은 화면에 표현 안되고 child 요소를 포함하는 Container의 용도로 사용됨 (XML UI에서 Containter는 화면에 표현 안되고 것과 같은 의미)
  • Preference XML 문서에서root가 아닌 child 요소로 사용 시 - Preference 아이템 중 하나로 취급. Preference 아이템 중 하나로 화면에 표시되며, 클릭 시 별도의 preference 화면으로 이동

 

!!! 그림 (main pref. -> sub pref)

 


 

▌PreferenceCategory 클래스▐

 

Preferences를 구성하는 객체들을 특정 category별로 분류 할 수 있게 하는 클래스. 중요한 XML 속성이나 메서드 없음

!!! 그림 (category 분류)

 

  


▌DialogPreference 클래스▐

 

모든 dialog 기반 Preference 객체의 부모 클래스. Preference 화면에는 링크만 표시되고, 링크를 클릭했을 때 실제 Preference를 컨트롤 할 수 있는 Dialog가 popup한다.

!!! 다이얼로그 그림

 

중요 XML 속성

  • android:dialogIcon – Dialog의 Icon 지정
  • android:dialogTitle – Dialog의 제목 지정
  • android:dialogMessage – Dialog 내에 표시될 문자열 내용 지정
  • android:negativeButton – 부정 버튼에 표시될 Text설정 (취소, cancel 등)
  • android:positiveButton – 긍정 버튼에 표시될 Text 설정 (적용, OK 등)

 

중요 메서드

  • void setDialogIcon(Drawable) – Dialog의 Icon 지정
  • void setDialogTitle(…) – Dialog의 제목 지정. 문자열 직접 지정 또는 문자열 리소스 사용
  • void setDialogMessage(…) – Dialog 내에 표시될 문자열 내용 지정
  • void setPositiveButtonText(…) – 부정 버튼에 표시될 Text설정 (취소, cancel 등)
  • void setNegativeButtonText(…) – 긍정 버튼에 표시될 Text 설정 (적용, OK 등)

  


 

▌EditeTextPreference 클래스▐

 

DialogPreference로부터 상속하며, 사용자로부터 문자열을 입력 받아 저장 하기 위한 Preference 아이템을 구현한 클래스.

 

중요 메서드

  • EditText getEditText() – Dialog가 포함하는 EditBox인스턴스를 리턴
  • String getText() – SharedPreferences 객체로부터 저장된 문자열 리턴
  • void setText() – SharedPreferences에 문자열 저장

  


 

▌ListPreference 클래스▐

 

DialogPreference로부터 상속하며, Dialog기반의 ListView 위젯에 미리 제공되는 문자열들을 표현하고 사용자가 그 문자열 중 하나를 선택하여 설정 값을 지정할 수 있도록 구현한 클래스.

 

중요 XML 속성

  • android:entries – ListView의 각 raw에 표현될 문자열을 array 리소스를 통해 공급. 사용자(human)를 위한 정보.
  • android:entryValues – ListView의 특정 raw가 사용자에 의해 선택되었을 때 프로그램 내부적으로 처리 할 문자열 data를 array 리소스 형식으로 제공. 컴퓨터가 처리 하기 위한 정보. (ex. 남/여: 사람을 위한 정보 -> 0/1: 컴퓨터를 위한 정보)
  • android:defaultValue – 초기 선택 항목 지정. (0-based 정수)

 

중요 메서드

  • CharSequence[] getEntries() – ListView의 각 row에 표현될 문자열들을 리턴 (사용자 위한 정보)
  • CharSequence[] getEntryValues() – Entry(사람을 위한 정보)와 연계된 컴퓨터가 처리할 문자열들을 리턴
  • void setEntries(…) – ListView의 각 row에 표현할 문자열 지정. Array리소스 또는 CharSequence[] 전달
  • void setEntryValues(…) – 컴퓨터가 처리할 문자열 지정. Array또는 CharSequence[] 전달

  


 

▌CheckBoxPreference 클래스▐

 

CheckBox 기능의 Preference 아이템을 구현한 클래스. SharedPreferences에 boolean 값으로 정보 저장

 

중요 XML 속성

  • android:summayOn – CheckBox가 check상태일 때 사용자에게 보일 안내문
  • android:summaryOff – CheckBox가 uncheck 상태일 때 사용자에게 보일 안내문

 

중요 메서드

  • boolean isChecked() – 현재 check/uncheck 상태 리턴
  • void setChecked(boolean) – CheckBox를 program 상에서 check/uncheck 함
  • void setSummaryOn(…) – CheckBox가 check상태일 때 사용자에게 보일 안내문 설정. String 리소스나 CharSequence 인스턴스 전달
  • void setSummaryOff(…) – CheckBox가 uncheck상태일 때 사용자에게 보일 안내문 설정

  


 

▌RingtonPreference▐

 

사용자가 디바이스에서 제공하는 전화 벨 소리를 지정할 수 있게 구현된 클래스. Intent를 이용해 어떤 벨 소리 Picker를 화면에 표시할 지 결정함.

 

중요 XML 속성

  • android:ringtoneType – 어떤 종류의 벨 소리 종류를 선택 가능하게 할지 지정. ringtone, notification, alarm, all 중에 하나 또는 복수(| 사용)개 지정 가능
  • android:showDefault – Default 벨소리 항목 표시 여부 결정 (true/false)
  • android:showSilent – 무음(Silent) 항목 표시 여부 결정 (true/false)

 

중요 메서드

  • setRingtoneType(int type) – XML 속성 중 ringtoneType에 대응. RingtoneManager 클래스에 선언되어 있는 상수 TYPE_RINGTONE, TYPE_NOTIFICATION, TYPE_ALARM, TYPE_ALL 중 하나 또는 복수(|사용) 전달
  • setShowDefault(boolean) – XML showDefault 속성에 대응
  • setShowSlient(boolean) – XML showSilent 속성에 대응

  


 

 PreferenceManager 클래스 

 

Activity나 XML로부터 Preferences 계층구조의 생성을 돕는 helper 클래스이지만 이 클래스를 이용하여 직접 Preferences를 구성하기보다는 PreferenceActivity.addPreferenceFromResource(int) 또는 PreferenceActivity.addPreferenceFromIntent(Intent)를 사용하는 것이 일반적이다.

 

혹시, PreferenceActivity를 사용하지 않고 Activity를 이용해 직접 Preferences 화면을 구성해야 한다면 필요한 클래스이다.

 

중요 메서드

  • Preference findPreference(CharSequence) – 인자로 전달되는 Key값을 가지는 Preference 항목의 인스턴스를 가져옴
  • SharedPreferences getDefaultSharedPreferences(Context) – 제공되는 context가 기본으로 사용하는 Preference파일로부터 SharedPreferences인스턴스 생성해 리턴
  • SharedPreferences getSharedPreferences() – this객체가 사용하는 context와 연결될 Preference파일로부터 SharedPreferences 인스턴스 생성해 리턴
  • void setDefaultValues(Context, int resId, boolean) – 제공되는 context가 사용하는 SharedPreferences를 두 번째 인자로 제공되는 XML 리소스에 정의된 기본 속성값들로 설정 함. 세 번째 인자가 false 일 경우엔 이 메서드가 전에 실행된 적이 있다면 무시하고, true일 경우 전에 실행 여부화 상관없이 재 실행된다. 참고: 마지막 인자를 true로 지정 했다고 해서 이 메서드를 이용해 Preferences를 바로 초기화 할 수 있는 것은 아니고, SharedPrefernces.clear() 메서드를 사용해 Preferences 항목을 모두 삭제 후 이 메서드를 사용해 XML에 지정된 본래의 값으로 Preferences를 초기화 할 수 잇다.

 

 

* 대충 필요해보이는 부분만 알아볼수있게 해석

Using the Core API in Python

The Core API exposes the fundamental building blocks that make up Dropbox. It's based on HTTP and OAuth, so it should be very familiar to most developers. There are faster and more powerful ways to integrate with Dropbox, such as Drop-ins and the Sync API, but if you need low-level control, the Core API is the best choice.

Core API는 HTTP와 OAuth에 기반을 둔 좀 더 기본적인 접근이다. 개발자들에게 더 익숙할 것임.

If you want to follow along, first register a new app on the App Console. You'll need the app key to access the Core API. Then install the Python SDK and you'll be ready to go.

처음 시작이라면 App Console 에서 새로운 앱을 등록해야함. Core API를 사용하려면 App Key가 필요하다. 그리고 python sdk 설치


Authenticating your app

The Core API uses OAuth v2, but the Python SDK will take care of most of it so you don't have to start from scratch.

You'll need to provide your app key and secret to the new DropboxOAuth2FlowNoRedirect object.

OAuth v2 파이썬이 알아서 해줄듯? 그리고 app_key, app_secret 발급 받자. DropboxOAuth2FlowNoRedirect

# Include the Dropbox SDK
import dropbox

# Get your app key and secret from the Dropbox developer website
app_key = 'INSERT_APP_KEY'
app_secret = 'INSERT_APP_SECRET'

flow = dropbox.client.DropboxOAuth2FlowNoRedirect(app_key, app_secret)

Now we're all set to start the OAuth flow. The OAuth flow has two parts:

  1. Ask the user to authorize linking your app to their Dropbox account.
  2. Once authorized, exchange the received authorization code for an access token, which will be used for calling the Core API.

OAuth Flow는 두개의 부분

1. 사용자 계정에 내 앱을 연결하도록 인증요청

2. 인증되면, Core API호츨을 위해 사용되는 접근 Token을 위한 인증코드를 교환함 .(?)

start 메소드로 인증 URL을 생성하는 DropboxOAuth2FlownoRedirect 를 사용하여 시작한다.

We'll start by using the DropboxOAuth2FlowNoRedirect object to generate an authorization URL with the start method.

authorize_url = flow.start()

With the authorization URL in hand, we can now ask the user to authorize your app. To avoid the hassle of setting up a web server in this tutorial, we're just printing the URL and asking the user to press the Enter key to confirm that they've authorized your app. However, in real-world apps, you'll want to automatically send the user to the authorization URL and pass in a callback URL so that the user is seamlessly redirected back to your app after pressing a button.

# Have the user sign in and authorize this token
authorize_url = flow.start()
print '1. Go to: ' + authorize_url
print '2. Click "Allow" (you might have to log in first)'
print '3. Copy the authorization code.'
code = raw_input("Enter the authorization code here: ").strip()

Once the user has delivered the authorization code to our app, we can exchange that code for an access token via finish:

# This will fail if the user enters an invalid authorization code
access_token, user_id = flow.finish(code)

The access token is all you'll need to make API requests on behalf of this user, so you should store it away for safe-keeping (even though we don't for this tutorial). By storing the access token, you won't need to go through these steps again unless the user reinstalls your app or revokes access via the Dropbox website.

Now that the hard part is done, all we need to do to authorize our API calls is to to pass the access token to DropboxClient. To test that we have got access to the Core API, let's try calling account_info, which will return a dictionary with information about the user's linked account:

client = dropbox.client.DropboxClient(access_token)
print 'linked account: ', client.account_info()

If you've made it this far, you now have a simple app that uses the Core API to link to a Dropbox account and make an API call to retrieve account info. Next, we'll upload a file to Dropbox, get its metadata, and then download it back to our app.

Uploading files

Let's say we're building a text editing app and we want to use it to save your latest magnum opus to Dropbox. Let's browse the methods in the Python docs to see which one will do that for us. This page lists all the methods supported in the SDK. If you scroll down, you'll find put_file.

put_file takes a path pointing to where we want the file on our Dropbox, and then a file-like object or string to be uploaded there. For this example, let's upload a local copy of working-draft.txt:

f = open('working-draft.txt')
response = client.put_file('/magnum-opus.txt', f)
print "uploaded:", response

If all goes well, the data in your local working-draft.txt will now be in the root of your app folder (or Dropbox folder, depending on your app's access type). The variable response will be a dictionary containing the metadata of the newly uploaded file. It will look something like this:

{
    'bytes': 77,
    'icon': 'page_white_text',
    'is_dir': False,
    'mime_type': 'text/plain',
    'modified': 'Wed, 20 Jul 2011 22:04:50 +0000',
    'path': '/magnum-opus.txt',
    'rev': '362e2029684fe',
    'revision': 221922,
    'root': 'dropbox',
    'size': '77 bytes',
    'thumb_exists': False
}

Listing folders

If you peruse the various entries in the metadata above, you'll get a good sense for all info we can gather from the file. You can get this info for an entire folder by using the metadata call.

folder_metadata = client.metadata('/')
print "metadata:", folder_metadata

The returned dictionary will list out the files and folders in the path given. It looks something like this:

{'bytes': 0,
 'contents': [{'bytes': 0,
               'icon': 'folder',
               'is_dir': True,
               'modified': 'Thu, 25 Aug 2011 00:03:15 +0000',
               'path': '/Sample Folder',
               'rev': '803beb471',
               'revision': 8,
               'root': 'dropbox',
               'size': '0 bytes',
               'thumb_exists': False},
              {'bytes': 77,
               'icon': 'page_white_text',
               'is_dir': False,
               'mime_type': 'text/plain',
               'modified': 'Wed, 20 Jul 2011 22:04:50 +0000',
               'path': '/magnum-opus.txt',
               'rev': '362e2029684fe',
               'revision': 221922,
               'root': 'dropbox',
               'size': '77 bytes',
               'thumb_exists': False}],
 'hash': 'efdac89c4da886a9cece1927e6c22977',
 'icon': 'folder',
 'is_dir': True,
 'path': '/',
 'root': 'app_folder',
 'size': '0 bytes',
 'thumb_exists': False}

In the example above, the app folder root contains a directory named Sample Folder and the file we just uploaded named magnum-opus.txt. You'll also see other various but vital bits of information such as the exact location of the file (path), file sizes (bytes), last modified date (modified), and more. If you want to tell if something has changed in the directory, you'll want to store and compare thehash parameter. Similarly, a file's rev parameter tells you if the file has changed. It's useful to keep track of the status of your files, as we'll see in the following example.

Downloading files

Some time has passed and you're ready to start editing that magnum opus of yours again. We'll need the get_file_and_metadatamethod to download the file.

f, metadata = client.get_file_and_metadata('/magnum-opus.txt')
out = open('magnum-opus.txt', 'w')
out.write(f.read())
out.close()
print metadata

get_file_and_metadata, like other calls that return file data, returns an httplib.HTTPResponse that you should .read() from to get the full response.

In addition to the file, the method also returns the file's metadata at its current revision. Every time a change is made to the file, the revfield of the file's metadata changes as well. By saving the revision when you download the file, you'll be able to tell if that file has been updated by another computer or device and choose to download the newer revision of that file.

The complete code

For those keeping score at home, here's the full source to this guide. Make sure to create a magnum-opus.txt file to get it to work fully. Also remember to insert your app key, app secret, and access type.

# Include the Dropbox SDK
import dropbox

# Get your app key and secret from the Dropbox developer website
app_key = 'INSERT_APP_KEY'
app_secret = 'INSERT_APP_SECRET'

flow = dropbox.client.DropboxOAuth2FlowNoRedirect(app_key, app_secret)

# Have the user sign in and authorize this token
authorize_url = flow.start()
print '1. Go to: ' + authorize_url
print '2. Click "Allow" (you might have to log in first)'
print '3. Copy the authorization code.'
code = raw_input("Enter the authorization code here: ").strip()

# This will fail if the user enters an invalid authorization code
access_token, user_id = flow.finish(code)

client = dropbox.client.DropboxClient(access_token)
print 'linked account: ', client.account_info()

f = open('working-draft.txt')
response = client.put_file('/magnum-opus.txt', f)
print 'uploaded: ', response

folder_metadata = client.metadata('/')
print 'metadata: ', folder_metadata

f, metadata = client.get_file_and_metadata('/magnum-opus.txt')
out = open('magnum-opus.txt', 'w')
out.write(f.read())
out.close()
print metadata

Python SDK Documentation

You can also dive into the Python SDK documentation, or if you're feeling ambitious, browse the Python SDK source code. The source code is found in the folder labeled dropbox within the SDK package. The file you'll be interfacing the most with is client.py. You won't need to deal with rest.py unless you intend to implement your own calls, but it's responsible for actually making API requests and parsing the server's response.


'공부 > Python' 카테고리의 다른 글

SQLAlchemy Tutorial(한글) - 2  (0) 2013.08.14
SQLAlchemy Tutorial(한글) - 1  (4) 2013.08.14

Fade, 이미지(CCSprite)를 점점 흐려지게 혹은 점점 진해지게 만들어 주는 액션이다.

 

※ 미리 선행되어야 할 작업 : setOpacity( 0~255가능 ) 메소드를 통해 Opacity값을 지정해주어야 함


■ FadeIn   : 현재의 opacity부터 최대opacity 255까지 지정한 duration 시간동안 점점 어두워짐 

                   ex) CCFadeIn*     in = CCFadeIn::actionWithDuration(1);

■ FadeOut : 현재의 opacity부터 최저 opacity 0까지 지정한 duration 시간동안 점점 밝아짐

                   ex) CCFadeOut* out = CCFadeOut::actionWithDuration(2);

■ FadeTo   : 현재의 opacity부터 내가 지정한 opacity까지 밝거나 혹은 어두워짐
                   ex) CCFadeTo* fadeto = CCFadeTo::actionWithDuration(3150);

 

   나는 메뉴가 선택되었을 때 팝업이 뜨면서 그 뒷 배경들은 점점 어두워지는 느낌을 표현하고 싶었다. 

   즉, 팝업은 FadeOut으로 밝아지고- 배경은 FadeIn으로 어두워지는것!

   그런데 내 생각과 다르게 FadeIn 메소드는 점점 어두워지면서 아예 까만색으로 아랫 노드를 완전히 가려버리는 문제가 있었다. 나는 뒤에 있는 배경을 반투명하게 가리고 싶은 건데 말이지. 알고보니 특정 Opacity까지만 내려가도록 설정해주는 FadeTo 라는 기특한 메소드가 있다. 현재의 opcity값과 메소드에 파라매터로 주어지는 Opacity값 중 어느 쪽이 크고 작냐에 따라 혼자서 FadeIn/Out 양쪽을 다 소화해낸다. 0 → 150 으로 Opacity를 주니 뒷 배경을 적절하게 가려주어서 만족.

 

   아래는 소스코드.

   쇼 팝업/ 하이드 팝업 메소드를 만들어 주고, 터치이벤트에서 해당 함수들을 호출하도록 한다. 배경을 반쯤 어둡게 덮는 레이어(alphaLayer)는 어두워지고, 팝업(popupbox)은 밝아진다. 랄까 팝업은 최초 Opacitiy가 0이라 더 밝아질 것도 없어서 '점점 밝아지는' 느낌은 안 나는데, 그런게 필요하다면 Opacity값을 수정하면 되겠다.

 

[팝업 컨트롤]

void XXX::showPopupBox(int menu) 
{

    alphaLayer->setIsVisible(true);

    popupBox->setIsVisible(true);

    
     // init에서 0값을 주었다고 여기에서 opacity셋팅을 빼먹으면

     // 처음 한 번만 페이드효과가 먹고, 그 이후부터는 페이드없이 바로 나타나는 불상사 발생 

    alphaLayer->setOpacity(0);

    popupBox ->setOpacity(0);

      

    CCFadeTo*  fadeto = CCFadeTo::actionWithDuration(2150);

    CCFadeIn*   fadein = CCFadeIn::actionWithDuration(2);   

  

    alphaLayer->runAction(fadeto);

    popupBox ->runAction(fadein);

}


void XXX::hidePopupBox() 
{

    popupBox->setIsVisible(false);

    alphaLayer->setIsVisible(false);

    // 이론적으로 opacty를 여기에서 0으로 주면 감춰져서 보이지 않을 것으로 생각했는데 그렇게 안 된다.

    // 왜일까- 잘 모르겠지만 우선 show와 hide에 각각 setIsVisible설정

}

 

[팝업 Sprite생성]

void XXX::init()
{ 

    ////////// Pop-up

   //////////////////////////////////////////////////////////////////////////////////////////

    popupBox = new CCSprite();

    popupBox->init();

    popupBox->setPosition(ccp(0,0));

    

    CCSprite* bgBox = CCSprite::spriteWithFile("pop_box.png");

    bgBox->setScale(0.7);

    bgBox->setPosition(ccp((float)screenSize.width/2, (float)screenSize.height/2));

    popupBox->addChild(bgBox,  10);

    

    btnCancle = CCMenuItemImage::itemFromNormalImage("btn_cancle.png","btn_cancle.png",

                                                                                this,menu_selector(EbookHome::popupCancleCallback) );

    btnCancle->setPosition(ccp((float)screenSize.width/2+210, (float)screenSize.height/2+80));

    

    btnRestart = CCMenuItemImage::itemFromNormalImage("btn_start.png","btn_start.png",

                                                                                this,menu_selector(EbookHome::popupRestartCallback) );

    btnRestart->setPosition(ccp((float)screenSize.width/2-100, (float)screenSize.height/2-40));

    

    btnResume = CCMenuItemImage::itemFromNormalImage("btn_resume.png","btn_resume.png",

                                                                                 this,menu_selector(EbookHome::popupResumeCallback) );

    btnResume->setPosition(ccp((float)screenSize.width/2+100, (float)screenSize.height/2-40));

    

    CCMenu* popupBtns = CCMenu::menuWithItems(btnCanclebtnRestartbtnResumeNULL);

    popupBtns->setPosition(ccp(0,0));

    popupBox->setOpacity(0);

    popupBox->addChild(popupBtns, 11);

    this->addChild(popupBox10);

    

    alphaLayer = new CCLayerColor();

    alphaLayer->initWithColor(ccc4f(0,0,0,0));

    alphaLayer->setOpacity(0);

    this->addChild(alphaLayer9);

    

    hidePopupBox();

}


출처 : http://deng-i.net/category/%EA%B3%B5%EB%B6%80%ED%95%98%EB%8A%94%20%EA%B0%9C%EB%B0%9C%EC%9E%90/Cocos%202d-x%20

앞서 작성한 SQLAlchemy Tutorial – Part 1에서 이어지는 번역이다.

(여기서 뭔가 모자란 부분이나 틀린게 있으면 틀린게 맞으므로 언제든 지적해주시고, 애매한 표현은 원본 문서를 봐주시면 감사하겠습니다. 원본 문서는 SQLAlchemy Tutorial. 한글로 된 sqlalchemy 튜토리얼 있으면 알려주세요!)


리스트와 Scalars 반환하기

Query 객체의 all()one()first() 메소드는 즉시 SQL을 호출하고 non-iterator 값을 반환한다.all()은 리스트를 반환한다.

query = session.query(User).filter(User.name.like('%air')). order_by(User.id)
query.all()
# [<User('haruair', 'Edward Kim', '1234')>, <User('wendy','Wendy Williams', 'foobar')>]

first()는 첫째를 리밋으로 설정해 scalar로 가져온다.

query.first()
# <User('haruair', 'Edward Kim', '1234')>

one()은 모든 행을 참조해 식별자를 값으로 가지고 있지 않거나 여러 행이 동일한 값을 가지고 있는 경우 에러를 만든다.

from sqlalchemy.orm.exc import MultipleResultsFound
try:
    user = query.one()
except MultipleResultsFound, e:
    print e


from sqlalchemy.orm.exc import NoResultFound
try:
    user = query.filter(User.id == 99).one()
except NoResultFound, e:
    print e

문자로 된 SQL 사용하기

문자열을 Query와 함께 유연하게 쓸 수 있다. 대부분 메소드는 문자열을 수용한다. 예를 들면filter()와 order_by()에서 쓸 수 있다.

for user in session.query(User).\
            filter("id<224").\
            order_by("id").all():
    print user.name

연결된 파라미터에서는 콜론을 이용한, 더 세세한 문자열 기반의 SQL를 사용할 수 있다. 값을 사용할 때 param() 메소드를 이용한다.

session.query(User).filter("id<:value and name=:name").\
    params(value=1234, name='fred').order_by(User.id).one()

문자열 기반의 일반적인 쿼리를 사용하고 싶다면 from_statement()를 쓴다. 대신 컬럼들은 매퍼에서 선언된 것과 동일하게 써야한다.

session.query(User).from_statement(
                    "SELECT * FROM users WHERE name=:name").\
                    params(name='haruair').all()

또한 from_statement() 아래와 같은 문자열 SQL 방식으로도 쓸 수 있다.

session.query("id", "name", "thenumber12").\
        from_statement("SELECT id, name, 12 as "
                "thenumber12 FROM users WHERE name=:name").\
        params(name='haruair').all()

문자열 SQL의 장단점

Query로 생성해서 쓰는건 sqlalchemy의 이점인데 그렇게 쓰지 않으면 당연히 안좋아지는 부분이 있다. 직접 쓰면 특정하게 자기가 필요한 결과물을 쉽게 만들어낼 수 있겠지만 Query는 더이상 SQL구조에서 아무 의미 없어지고 새로운 문맥으로 접근할 수 있도록 변환하는 능력이 상실된다.

예를 들면 User 객체를 선택하고 name 컬럼으로 정렬하는데 name이란 문자열을 쓸 수 있다.

q = session.query(User.id, User.name)
q.order_by("name").all()

지금은 문제 없다. Query를 쓰기 전에 뭔가 멋진 방식을 사용해야 할 때가 있다. 예를 들면 아래처럼from_self() 같은 고급 메소드를 사용해, 사용자 이름의 길이가 다른 경우를 비교할 때가 있다.

from sqlalchemy import func
ua = aliased(User)
q = q.from_self(User.id, User.name, ua.name).\
    filter(User.name < ua.name).\
    filter(func.length(ua.name) != func.length(User.name))

Query는 서브쿼리에서 불러온 것처럼 나타나는데 User는 내부와 외부 양쪽에서 불러오게 된다. 이제Query에게 name으로 정렬하라고 명령하면 어느 name을 기준으로 정렬할지 코드로는 예측할 수 없게 된다. 이 경우에는 바깥과 상관없이 aliased된 User를 기준으로 정렬된다.

q.order_by("name").all()
# [(3, u'fred', u'haruair'), (4, u'haruair', u'mary'), (2, u'mary', u'wendy'), (3, u'fred', u'wendy'), (4, u'haruair', u'wendy')]

User.name 또는 ua.name 같이 SQL 요소를 직접 쓰면 Query가 알 수 있을 만큼 충분한 정보를 제공하기 때문에 어떤 name을 기준으로 정렬해야할지 명확하게 판단하게 된다. 그래서 아래 두가지와 같은 차이를 볼 수 있다.

q.order_by(ua.name).all()
# [(3, u'fred', u'haruair'), (4, u'haruair', u'mary'), (2, u'mary', u'wendy'), (3, u'fred', u'wendy'), (4, u'haruair', u'wendy')]

q.order_by(User.name).all()
# [(3, u'fred', u'wendy'), (3, u'fred', u'haruair'), (4, u'haruair', u'wendy'), (4, u'haruair', u'mary'), (2, u'mary', u'wendy')]

숫자세기

Query는 count()라는 숫자를 세는 편리한 메소드를 포함한다.

session.query(User).filter(User.name.like('haru%')).count()

count()는 몇개의 행이 반환될지 알려준다. 위 코드로 생성되는 SQL을 살펴보면, SQLAlchemy는 항상 어떤 쿼리가 오더라도 거기서 행의 수를 센다. SELECT count(*) FROM table 하면 단순해지지만 최근 버전의 SQLAlchemy는 정확한 SQL로 명시적으로 판단할 수 있는 경우 추측해서 처리하지 않는다.

숫자를 세야 할 필요가 있는 경우에는 func.count()로 명시적으로 작성하면 된다.

from sqlalchemy import func
session.query(func.count(User.name), User.name).group_by(User.name).all()
# [(1, u'fred'), (1, u'haruair'), (1, u'mary'), (1, u'wendy')]

SELECT count(*) FROM table만 하고 싶으면

session.query(func.count('*')).select_from(User).scalar()

User의 primary key를 사용하면 select_from 없이 사용할 수 있다.

session.query(func.count(User.id)).scalar() 

관계(relationship) 만들기

이제 User와 관계된, 두번째 테이블을 만들 것이다. 계정당 여러개 이메일 주소를 저장할 수 있게 만들 것이다. users 테이블과 연결되는, 일대다 테이블이므로 테이블 이름을 addresses라고 정하고 전에 작성했던 것처럼 Declarative로 address 클래스를 작성한다.

from sqlalchemy import ForeignKey
from sqlalchemy.orm import relationship, backref

class Address(Base):
    __tablename__ = 'addresses'
    id = Column(Integer, primary_key=True)
    email_address = Column(String, nullable=False)
    user_id = Column(Integer, ForeignKey('users.id'))

    user = relationship("User", backref=backref('addresses', order_by=id))

    def __init__(self, email_address):
        self.email_address = email_address

    def __repr__(self):
        return "<Address('%s')>" % self.email_address

위 클래스는 ForeignKey를 어떻게 만드는지 보여준다. Column에 직접 넣은 지시자는 이 컬럼의 내용이 대상된 컬럼을 따르도록 만든다. 이 점이 관계 데이터베이스의 주요 특징 중 하나인데 풀과 같은 역할을 해, 연결되지 않은 테이블 사이를 잘 붙여준다. 위에서 작성한 ForeignKey는 addresses.user_id컬럼이 users.id 컬럼을 따르도록 만든다.

두번째 지시자인 relationship()은 ORM에게 Address 클래스 자체가 User 클래스에 연결되어 있다는 사실을 Address.user 속성을 이용해 알 수 있게 해준다. relationship()은 외래키 연결에서 두 테이블 사이에 Address.user로 다대일 관계임을 결정한다.

덧붙여 relationship()내에서 호출하는 backref()는 역으로 클래스를 이용할 수 있도록, 즉 Address객체에서 User를 참조할 수 있도록 User.addresses를 구현한다. 다대일 관계의 반대측은 항상 일대다의 관계이기 때문이다. 자세한건 기본 관계 패턴 문서를 참고.

Address.user와 User.addresses의 관계는 양방향 관계(bidirectional relationship) SQLAlchemy ORM의 주요 특징이다. Backref로 관계 연결하기 에서 backref에 대한 자세한 정보를 확인할 수 있다.

relationship()을 원격 클래스를 객체가 아닌 문자열로 연결하는 것에 대해 Declarative 시스템에서 사용하는 것으로 문제가 될 수 있지 않나 생각해볼 수 있다. 전부 맵핑이 완료된 경우, 이런 문자열은 파이썬 표현처럼 다뤄지며 실제 아규먼트를 처리하기 위해 사용된다. 위의 경우에선 User 클래스가 그렇다. 이런 이름들은 이것이 만들어지는 동안에만 허용되고 모든 클래스 이름은 기본적으로 선언될 때 사용이 가능해진다. (주. 클래스의 선언이 순차적으로 진행되기 때문에 클래스 선언 이전엔 에러가 나므로 이런 방식을 사용하는 것으로 보인다.)

아래는 동일하게 “addresses/user” 양방향 관계를 User 대신 Address로 선언한 모습이다.

class User(Base):
    # ...
    addresses = relationship("Address", order_by="Address.id", backref="user")

상세한 내용은 relationship()를 참고.

이건 알고 계시나요?

  • 대부분의 관계형 데이터베이스에선 외래키 제약이 primary key 컬럼이나 Unique 컬럼에만 가능하다.
  • 다중 컬럼 pirmary key에서의 외래키 제약은 스스로 다중 컬럼을 가지는데 이를 합성외래키(composite foreign key)라고 한다. 이 또한 이 컬럼의 서브셋을 레퍼런스로 가질 수 있다.
  • 외래키 컬럼은 연결된 컬럼이나 행의 변화에 자동으로 그들 스스로를 업데이트 한다. 이걸 CASCADE referential action이라고 하는데 관계형 데이터베이스에 내장된 함수다.
  • 외래키는 스스로의 테이블을 참고할 수 있다. 이걸 자기참조(self-referential) 외래키라고 한다.
  • 외래키에 대해 더 알고 싶다면 위키피디아 외래키 항목을 참고.

addresses 테이블을 데이터베이스에 생성해야 하므로 metadata로부터 새로운 CREATE를 발행한다. 이미 생성된 테이블은 생략하고 생성한다.

Base.metadata.create_all(engine)

관계된 객체 써먹기

이제 User를 만들면 빈 addresses 콜렉션이 나타난다. 딕셔너리나 set같은 다양한 컬랙션이 있는데 기본으로 컬랙션은 파이썬의 리스트다. (컬렉션 접근을 커스터마이징 하려면 이 문서 참고)

jack = User('jack', 'Jack Bean', 'sadfjklas')
jack.addresses # [] 빈 리스트를 반환

자유롭게 Address 객체를 User 객체에 넣을 수 있다. 그냥 리스트 사용법이랑 똑같다.

jack.addresses = [
                Address(email_address='jack@gmail.com'),
                Address(email_address='jack@yahoo.com')]

양방향 관계인 경우 자동으로 양쪽에서 접근할 수 있게 된다. 별도의 SQL 없이 양쪽에 on-change events로 동작한다.

jack.addresses[1]       # <Address(email_address='jack@yahoo.com')>
jack.addresses[1].user  # <User('jack', 'Jack Bean', 'sadfjklas')>user는 user = relationship("User", backref=backref('addresses', order_by=User.id))

데이터베이스에 저장해보자. User인 Jack Bean을 저장하면 두 Address도 알아서 cascading으로 저장된다.

session.add(jack)
session.commit()

Jack을 쿼리해서 다시 불러보자. 이렇게 Query하면 아직 주소들은 SQL을 호출하지 않은 상태다.

jack = session.query(User).\
filter_by(name='jack').one()
Jack        # <User('jack', 'Jack Bean', 'sadfjklas')>

하지만 addressses 컬랙션을 호출하는 순간 SQL이 만들어진다.

jack.addresses
# [<Address(email_address='jack@gmail.com')>, <Address(email_address='jack@yahoo.com')>]

이렇게 뒤늦게 SQL로 불러오는걸 게으른 불러오기 관계(lazy loading relationship)라고 한다. 이addresses는 이제 불러와 평범한 리스트처럼 동작한다. 이렇게 컬랙션을 불러오는 방법을 최적화하는 방법은 나중에 살펴본다.

Join과 함꼐 쿼리하기

두 테이블이 있는데 Query의 기능으로 양 테이블을 한방에 가져오는 방법을 살펴볼 것이다. SQL JOIN에 대해 join 하는 방법과 여러가지 좋은 설명이 위키피디아에 있으니 참고.

간단하게 User와 Address 두 테이블을 완전 조인하는 방법은 Query.filter()로 관계있는 두 컬럼이 동일한 경우를 찾으면 된다.

for u, a in session.query(User, Address).\
                    filter(User.id==Address.user_id).\
                    filter(Address.email_address=='jack@gmail.com').\
                    all():
    print u, a
# <User('jack', 'Jack Bean', 'sadfjklas')> <Address('jack@gmail.com')>

반면 진짜 SQL JOIN 문법을 쓰려면 Query.join()을 쓴다.

session.query(User).join(Address).\
        filter(Address.email_address=='jack@gmail.com').\
        all()
# [<User('jack', 'Jack Bean', 'sadfjklas')>]

Query.join()은 User와 Address 사이에 있는 하나의 외래키를 기준으로 join한다. 만약 외래키가 없거나 여러개라면 Query.join() 아래같은 방식을 써야한다.

query.join(Address, User.id==Address.user_id)   # 정확한 상태를 적어줌
query.join(User.addresses)                      # 명확한 관계 표기 (좌에서 우로)
query.join(Address, User.addresses)             # 동일, 명확하게 목표를 정해줌
query.join('addresses')                         # 동일, 문자열 이용

외부 join은 outerjoin()을 쓴다.

query.outerjoin(User.addresses)     # left outer join

join()이 궁금하면 문서를 참고하자. 어떤 SQL에서든 짱짱 중요한 기능이다.

별칭(aliases) 사용하기

여러 테이블을 쿼리하면 같은 테이블을 여러개 불러와야 할 떄가 있는데 그럴 때 동일 테이블명에 별칭(alias)를 지정해 다른 테이블과 문제를 이르키지 않도록 해야한다. Query는 별칭으로 된 녀석들도 잘 알아서 처리해준다. 아래 코드는 Address 엔티티를 두번 조인해서 한 행에 두 이메일 주소를 가져오도록 하는 예시다.

from sqlalchemy.orm import aliased
adalias1 = aliased(Address)
adalias2 = aliased(Address)
for username, email1, email2 in \
    session.query(User.name, adalias1.email_address, adalias2.email_address).\
    join(adalias1, User.addresses).\
    join(adalias2, User.addresses).\
    filter(adalias1.email_address=='jack@gmail.com').\
    filter(adalias2.email_address=='jack@yahoo.com'):
    print username, email1, email2
# jack jack@gmail.com jack@yahoo.com

서브쿼리 사용하기

Query는 서브쿼리 만들 때에도 유용하다. User 객체가 몇개의 Address를 가지고 있는지 알고 싶을 때 서브쿼리는 유용하다. SQL을 만드는 방식으로 생각하면 주소 목록의 수를 사용자 id를 기준으로 묶은 후(grouped by), User와 join하면 된다. 이 상황에선 LEFT OUTER JOIN이 사용자의 모든 주소를 가져오므로 적합하다. SQL의 예를 보자.

SELECT users.*, adr_count.address_count
FROM users
LEFT OUTER JOIN (
        SELECT user_id, count(*) AS address_count
        FROM addresses GROUP BY user_id
    ) AS adr_count
    ON users.id = adr_count.user_id

Query를 사용하면 명령문을 안에서 밖으로 빼내듯 쓸 수 있다. 명령문 접근자는 일반적인 Query를 통해 SQL 표현을 나타내는 명령문을 생성해 반환한다. 이건 select()를 쓰는 것과 비슷한데 자세한건SQL 표현 언어 튜토리얼 문서를 참고.

from sqlalchemy.sql import func
stmt = session.query(Address.user_id, func.count('*').label('address_count')).\
        group_by(Address.user_id).subquery()

func 키워드는 SQL 함수를 만들고 subquery() 메소드는 별칭을 이용해 다른 query에 포함할 수 있는 SELECT 명령문의 형태로 반환해준다. (query.statement.alias()를 줄인 것)

이렇게 만든 서브쿼리는 Table처럼 동작한다. 아래 코드를 잘 모르겠으면 튜토리얼 앞부분에서 Table을 어떻게 다뤘는지 살펴보면 도움이 된다. 여기서는 컬럼에 접근할 때 table.c.컬럼명으로 접근했던, 그 방법처럼 사용한다.

for u, count in session.query(User, stmt.c.address_count).\
    outerjoin(stmt, User.id==stmt.c.user_id).order_by(User.id):
    print u, count
# <User('wendy', 'Wendy Williams', 'foobar')> None
# <User('mary', 'Mary Contrary', 'xxg527')> None
# <User('fred', 'Fred Flinstone', 'blar')> None
# <User('haruair', 'Edward Kim', '1234')> None
# <User('jack', 'Jack Bean', 'sadfjklas')> 2

서브쿼리서 엔티티 선택하기

위에서는 서브쿼리서 컬럼을 가져와서 결과를 만들었다. 만약 서브쿼리가 엔티티를 선택하기 위한 맵이라면 aliased()로 매핑된 클래스를 서브쿼리로 활용할 수 있다.

stmt = session.query(Address).\
                filter(Address.email_address != 'jack@yahoo.com').\
                subquery()
adalias = aliased(Address, stmt)
for user, address in session.query(User, adalias).\
        join(adalias, User.addresses):
    print user, address
# <User('jack', 'Jack Bean', 'sadfjklas')> <Address('jack@gmail.com')>

EXISTS 사용하기

SQL에서 EXISTS 키워드는 불린 연산자로 조건에 맞는 행이 있으면 True를 반환한다. 이건 많은 시나리오에서 join을 위해 쓰는데, join에서 관계 테이블서 적합한 값이 없는 행을 처리하는데에도 유용하다.

외부 EXISTS는 이런 방식으로 할 수 있다.

from sqlalchemy.sql import exists
stmt = exists().where(Address.user_id==User.id)
for name, in session.query(User.name).filter(stmt):
    print name
# jack

Query의 기능 중 몇가지 연산자에서는 EXISTS를 자동으로 사용한다. 위 같은 경우는 User.addresses관계에 any()를 사용하면 가능하다.

for name, in ssession.query(User.name).\
        filter(User.addresses.any()):
    print name
# jack

any()는 특정 기준이 있어 제한적으로 매치해준다.

for name, in session.query(User.name).\
    filter(User.addresses.any(Address.email_address.like('%gmail%'))):
    print name
# jack

has()도 any()와 동일한 기능을 하는데 대신 다대일 관계에서 사용한다. (~연산자는 NOT이란 뜻이다.)

session.query(Address).\
    filter(~Address.user.has(User.name=='jack')).all()
# []

일반 관계 연산자

관계(relationship)에서 사용할 수 있는 모든 연산자인데 각각 API 문서에서 더 자세한 내용을 볼 수 있다.

__eq__() 다대일에서의 equals 비교

query.filter(Address.user == somuser)

__ne__() 다대일에서의 not equals 비교

query.filter(Address.user != someuser)

IS NULL 다대일 비교 (__eq__())

query.filter(Address.user == None)

contains() 일대다 컬렉션에서 사용

query.filter(User.addresses.contains(someaddress))

any() 컬렉션에서 사용

query.filter(User.addresses.any(Address.email_address == 'bar'))

# 키워드 아규먼트도 받음
query.filter(User.addresses.any(email_address='bar'))

has() 스칼라 레퍼런스서 사용 (뭐지 ㅜㅠ)

query.filter(Address.user.has(name='ed'))

Query.with_parent() 어던 관계서든 사용

session.query(Address).with_parent(someuser, 'addresses')

선행 로딩 (Eager Loading)

lazy loading의 반대 개념으로 User.addresses를 User 호출할 때 바로 불러오도록 하는 방법이다. eager loading으로 바로 불러오면 쿼리 호출의 수를 줄일 수 있다. SQLAlchemy는 자동화와 사용자정의 기준을 포함해 3가지 타입의 선행 로딩(eager loading)를 제공한다. 3가지 모두 query options로 제어하는데 Query에 불러올 때 Query.options() 메소드를 통해 쓸 수 있다.

서브쿼리 로딩

선행로딩하도록 User.addresses에 표기하는 방법이다. orm.subqueryload()를 이용해서 서브쿼리를 불러올 떄 한번에 연계해 불러오도록 처리한다. 기존의 서브쿼리는 재사용이 가능한 형태지만 이것는 바로 Query를 거쳐 선택되기 때문에 관계된 테이블을 선택하는 것과 상관없이 서브쿼리가 동작한다. 복잡해보이지만 아주 쉽게 쓸 수 있다.

from sqlalchemy.orm import subqueryload
jack = session.query(User).\
                options(subqueryload(User.addresses)).\
                filter_by(name='jack').one()
jack
# <User('jack', 'Jack Bean', 'sadfjklas')>
jack.addresses
# [<Address('jack@gmail.com')>, <Address('jack@yahoo.com')>]

연결된 로딩 (Joined Load)

또 다른 자동 선행로딩 함수로 orm.joinedload()가 있다. join할 때 사용할 수 있는 방법으로 관계된 객체나 컬렉션을 불러올 때 한번에 불러올 수 있다. (LEFT OUTER JOIN이 기본값) 앞서의 addresses를 동일한 방법으로 불러올 수 있다.

from sqlalchemy.orm import joinedload

jack = session.query(User).\
                options(joinedload(User.addresses)).\
                filter_by(name='jack').one()
jack
# <User('jack', 'Jack Bean', 'sadfjklas')>
jack.addresses
# [<Address('jack@gmail.com')>, <Address('jack@yahoo.com')>]

사실 OUTER JOIN 결과라면 두 행이 나타나야 하는데 여전히 User 하나만 얻을 수 있다. 이 이유는Query는 엔티티를 반환할 때 객체 유일성을 위해 “유일하게 하기(uniquing)” 전략을 취한다.

joinedload()는 오랜동안 써왔지만 subqueryload() 메소드가 더 새로운 형태의 선행로딩 형태다. 둘 다 한 행을 기준으로 관계된 객체를 가져오는 것은 동일하지만 subqueryload()는 적합한 관계 컬렉션을 가져오기에 적합하고 반면 joinedload()가 다대일 관계에 적합하다.

joinedload()는 join()의 대체재가 아니다.

joinedload()으로 join을 생성하면 익명으로 aliased되어 쿼리 결과에 영향을 미치지 않는다.Query.order_by()나 Query.filter() 호출로 이런 aliased된 테이블을 참조할 수 없기 때문에 사용자 공간에서는 Query.join()을 사용해야 한다. joinedload()은 단지 관계된 객체 또는 콜랙션의 최적화된 내역을 불러오기 위해 사용하는 용도이기 때문에 추가하거나 제거해도 실제 결과엔 영향을 미치지 않는다. 더 궁금하면 선행 로딩의 도를 참고.

명시적 Join + 선행로딩

세번째 스타일의 선행 로딩은 명시적 Join이 primary 행에 위치했을 때 추가적인 테이블에 관계된 객체나 컬렉션을 불러온다. 이 기능은 orm.contains_eager()를 통해 제공되는데 다대일 객체를 미리 불러와 동일 객체에 필터링 할 경우에 유용하게 사용된다. 아래는 Address행에 연관된 User 객체를 가져오는 코드인데 “jack”이란 이름의 User를 orm.contains_eager()를 사용해 user 컬럼을 Address.user속성으로 선행로딩한다.

from sqlalchemy.orm import contains_eager
jack_addresses = session.query(Address).\
                            join(Address.user).\
                            filter(User.name=='jack').\
                            options(contains_eager(Address.user)).\
                            all()
jack_addresses
# [<Address('jack@gmail.com')>, <Address('jack@yahoo.com')>]
jack_addresses[0].user
# <User('jack', 'Jack Bean', 'sadfjklas')>

기본적으로 어떻게 불러오는지 설정하는 다양한 방법 등 선행 로딩의 추가적인 정보는 관계 불러오기 테크닉 문서를 참고.

삭제하기

jack을 삭제해보자. 삭제하고나면 count는 남은 행이 없다고 표시한다.

session.delete(jack)
session.query(User).filter_by(name='jack').count()
# 0

여기까진 좋다. Address 객체는 어떤지 보자.

session.query(Address).filter(
    Address.email_address.in_(['jack@gmail.com','jack@yahoo.com'])
).count()
# 2

여전히 남아있다. SQL을 확인해보면 해당 Address의 user_id 컬럼은 모두 NULL로 되어 있지만 삭제되진 않았다. SQLAlchemy는 제거를 종속적으로(cascade) 하지 않는데 필요로 한다면 그렇게 할 수 있다.

삭제/삭제-외톨이 종속처리 설정하기

cascade 옵션을 변경하기 위해서는 User.addresses의 관계에서 행동을 변경시켜야 한다. SQLAlchemy는 새 속성을 추가하는 것과 관계를 맵핑하는 것은 언제나 허용되지만 이 경우에는 존재하는 관계를 제거하는게 필요하므로 맵핑을 완전히 새로 시작해야한다. 먼저 Session을 닫는다.

session.close()

그리고 새 declarative_base()를 사용한다.

Base = declarative_base()

다음으로 User 클래스를 선언하고 addresses 관계를 종속처리 설정과 함께 추가한다. (생성자는 대충 두자)

class User(Base):
    __tablename__ = 'users'

    id = Column(Integer, primary_key=True)
    name = Column(String)
    fullname = Column(String)
    password = Column(String)

    addresses = relationship("Address", backref='user', cascade="all, delete, delete-orphan")

    def __repr__(self):
        return "<User('%s','%s','%s'>" % (self.name, self.fullname, self.password)

그리고 Address도 다시 생성한다. 이 경우에는 이미 User에서 관계를 생성했기 때문에Address.user는 따로 생성할 필요가 없다.

class Address(Base):
    __tablename__ = 'addresses'
    id = Column(Integer, primary_key=True)
    email_address = Column(String, nullable=False)
    user_id = Column(Integer, ForeignKey('users.id'))

    def __repr__(self):
        return "<Address('%s')>" % self.email_address

이제 Jack을 불러오고 삭제하면 Jack의 addresses 컬랙션은 Address에서 삭제된다.

# jack을 primary key로 불러옴
jack = session.query(User).get(5)
# 첫 Address를 삭제 (지연 로딩이 동작한다)
del jack.addresses[1]
# address는 하나만 남는다
session.query(Address).filter(
    Address.email_address.in_(['jack@gmail','jack@yahoo.com'])
).count()
# 1

Jack을 지우면 Jack과 남은 Address도 삭제된다.

session.delete(jack)
session.query(User).filter_by(name='jack').count()
# 0
session.query(Address).filter(
    Address.email_address.in_(['jack@gmail.com','jack@yahoo.com'])
).count()
# 0

종속처리(cascade)에 대해

종속처리에 대한 더 자세한 설정은 Cascades 문서를 참고. 종속처리는 함수적으로 관련된 데이터베이스가 자연스럽게 ON DELETE CASCADE될 수 있도록 통합할 수 있다. Using Passive Deletes 문서 참고

다대다 관계(Many To Many Relationship) 만들기

일종의 보너스 라운드로 다대다 관계를 만드는 방법을 살펴본다. 블로그와 같은걸 만들 때를 예로 들면 BlogPost와 그에 따른 Keyword를 조합해야 하는 경우가 있다.

평범한 다대다 관계를 위해, 맵핑되지 않은 Table 구조를 조합 테이블로 만들 수 있다.

from sqlalchemy import Table, Text
# 조합 테이블
post_keywords = Table('post_keywords', Base.metadata,
    Column('post_id', Integer, ForeignKey('posts.id')),
    Column('keyword_id', Integer, ForeignKey('keywords.id'))
)

위 코드는 맵핑된 클래스를 선언하는 것과는 약간 다르게 Table를 직접 선언했다. Table은 생성자 함수로 각각 개별의 Column 아규먼트를 쉼표(comma)로 구분한다. Column 객체는 클래스의 속성명을 가져오는 것과 달리 이름을 명시적으로 작성해준다.

다음은 BlogPost와 Keyword를 relationship()으로 post_keywords 테이블에 연결해 정의한다.

class BlogPost(Base):
    __tablename__ = 'posts'

    id = Column(Integer, primary_key=True)
    user_id = Column(Integer, ForeignKey('users.id'))
    headline = Column(String(255), nullable=False)
    body = Column(Text)
    # 다대다 관계 : BlogPost <-> Keyword
    keywords = relationship('Keyword', secondary=post_keywords, backref='posts')

    def __init__(self, headline, body, author):
        self.author = author
        self.headline = headline
        self.body = body

    def __repr__(self):
        return "<BlogPost('%r', '%r', '%r')>" % (self.headline, self.body, self.author)

class Keyword(Base):
    __tablename__ = 'keywords'

    id = Column(Integer, primary_key=True)
    keyword = Column(String(50), nullable=False, unique=True)

    def __init__(self,keyword):
        self.keyword = keyword

위에서 BlogPost.keywords는 다대다 관계다. 다대다 관계를 정의하는 기능은 secondary 키워드로 연관 테이블인 Table객체를 참조한다. 이 테이블은 단순히 양측의 관계를 참고하는 형태며 만약 다른 컬럼이 있다면, 예를 들어 자체 primary key가 있거나 foreign key를 가진다면 연관 객체(association object) 라는 다른 형태의 사용패턴을 사용해야 한다. 연관 객체 문서 참고.

그리고 BlogPost 클래스는 author필드를 가진다. 그래서 다른 양방향 관계를 만들 것인데 단일 사용자가 엄청나게 많은 블로그 포스트를 가질 수 있다는 문제점을 처리해야한다. 다시 말해User.posts에 접근하면 모든 포스트를 불러올 것이 아니라 일부 필터된 결과만 가져와야 한다. 이런 경우를 위해 relationship()은 lazy='dynamic'을 지원하는데 속성을 불러오는 전략의 대안 중 하나다. 이것을 relationship()의 역방향으로 사용하려면 backref()를 사용하면 된다.

from sqlalchemy.orm import backref
# User에서의 관계를 "다이나믹" 로딩 처리
BlogPost.author = relationship(User, backref=backref('posts', lazy='dynamic'))

그리고 새 테이블을 생성한다.

Base.meta.create_all(engine)

사용 방법은 크게 다르지 않다.

wendy = session.query(User).\
                filter_by(name='wendy').\
                one()
post = BlogPost("Wendy's Blog Post", "This is a test", wendy)
session.add(post)

지금 키워드는 데이터베이스에 각각 유일하게 저장한다. 아직 뭔가 거창한걸 한건 아니고 그냥 생성할 뿐이다.

post.keywords.append(Keyword('wendy'))
post.keywords.append(Keyword('firstpost')) 

이제 키워드가 ‘firstpost’인 모든 글을 찾아볼 것이다. 여기서 any 연산자로 ‘firstpost’인 글을 찾는다.

session.query(BlogPost).\
        filter(BlogPost.keywords.any(keyword='firstpost')).\
        all()
# [BlogPost("Wendy's Blog Post", 'This is a test', <User('wendy','Wendy Williams', 'foobar')>)]

만약 Wendy의 포스트만 보고 싶다면,

session.query(BlogPost).\
        filter(BlogPost.author=wendy).\
        filter(BlogPost.keywords.any(keyword='firstpost')).\
        all()
# [BlogPost("Wendy's Blog Post", 'This is a test', <User('wendy','Wendy Williams', 'foobar')>)]

또는 Wendy가 소유하고 있는 posts 관계 즉 dyanmic 관계를 이용해 불러오는 방법은 아래와 같다.

wendy.posts.\
    filter(BlogPost.keywords.any(keyword='firstpost')).\
    all()
# [BlogPost("Wendy's Blog Post", 'This is a test', <User('wendy','Wendy Williams', 'foobar')>)]

이후 읽어볼 만한 문서

(주. 아마 아래 문서 중 세션과 관계 문서를 먼저 옮길 것 같습니다.)


'공부 > Python' 카테고리의 다른 글

Dropbox API 사용하기 (with python)  (0) 2013.08.16
SQLAlchemy Tutorial(한글) - 1  (4) 2013.08.14

+ Recent posts